├── doc ├── nip.pdf ├── vi95_lewis.pdf ├── briechle_spie2001.pdf └── mics2012_submission_34.pdf ├── image ├── test1.jpg ├── test2.jpg ├── test3.jpg ├── test4.jpg └── test5.jpg ├── README.md ├── .gitignore ├── src ├── main │ └── java │ │ └── info │ │ └── hb │ │ ├── lookup │ │ └── object │ │ │ ├── common │ │ │ ├── GFirst.java │ │ │ ├── FontFamily.java │ │ │ ├── ImageBinary.java │ │ │ ├── GPoint.java │ │ │ ├── ImageBinaryFeature.java │ │ │ ├── FeatureSet.java │ │ │ ├── GSPoint.java │ │ │ ├── ImageMultiplySum.java │ │ │ ├── IntegralImage.java │ │ │ ├── FontSymbol.java │ │ │ ├── ImageZeroMean.java │ │ │ ├── Feature.java │ │ │ ├── ImageMultiply.java │ │ │ ├── ImageBinaryGreyScale.java │ │ │ ├── ImageBinaryGreyScaleRGB.java │ │ │ ├── GFirstLeftRight.java │ │ │ ├── GrayImage.java │ │ │ ├── ImageBinaryChannelFeature.java │ │ │ ├── FontSymbolLookup.java │ │ │ ├── RGBImage.java │ │ │ ├── FeatureSetAuto.java │ │ │ ├── ImageBinaryGrey.java │ │ │ ├── FeatureSetDefault.java │ │ │ ├── LessCompare.java │ │ │ ├── IntegralImage2.java │ │ │ ├── ImageBinaryRGB.java │ │ │ ├── ImageBinaryGreyFeature.java │ │ │ ├── ImageBinaryScale.java │ │ │ ├── ImageBinaryChannel.java │ │ │ ├── SArray.java │ │ │ ├── ImageBinaryRGBFeature.java │ │ │ ├── RectK.java │ │ │ ├── RangeColor.java │ │ │ ├── ClassResources.java │ │ │ └── FeatureK.java │ │ │ ├── OCRScale.java │ │ │ ├── OCRCore.java │ │ │ ├── proc │ │ │ ├── FNCC.java │ │ │ ├── NCC.java │ │ │ └── CannyEdgeDetector.java │ │ │ ├── README.md │ │ │ ├── LookupColor.java │ │ │ ├── LookupScale.java │ │ │ ├── OCR.java │ │ │ ├── Capture.java │ │ │ └── Lookup.java │ │ ├── tesseract │ │ └── example │ │ │ ├── TesseractDemo.java │ │ │ └── Demo1.java │ │ └── ocr │ │ └── example │ │ └── ImageScanner.java └── test │ └── java │ └── info │ └── hb │ └── lookup │ └── object │ ├── LookupTest.java │ ├── SArrayTest.java │ ├── NCCTest.java │ ├── FNCCTest.java │ ├── OCRTest.java │ ├── SNCCTest.java │ └── OCRScaleTest.java └── pom.xml /doc/nip.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepCompute/image-text-extract/HEAD/doc/nip.pdf -------------------------------------------------------------------------------- /image/test1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepCompute/image-text-extract/HEAD/image/test1.jpg -------------------------------------------------------------------------------- /image/test2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepCompute/image-text-extract/HEAD/image/test2.jpg -------------------------------------------------------------------------------- /image/test3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepCompute/image-text-extract/HEAD/image/test3.jpg -------------------------------------------------------------------------------- /image/test4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepCompute/image-text-extract/HEAD/image/test4.jpg -------------------------------------------------------------------------------- /image/test5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepCompute/image-text-extract/HEAD/image/test5.jpg -------------------------------------------------------------------------------- /doc/vi95_lewis.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepCompute/image-text-extract/HEAD/doc/vi95_lewis.pdf -------------------------------------------------------------------------------- /doc/briechle_spie2001.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepCompute/image-text-extract/HEAD/doc/briechle_spie2001.pdf -------------------------------------------------------------------------------- /doc/mics2012_submission_34.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepCompute/image-text-extract/HEAD/doc/mics2012_submission_34.pdf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## 图片文字提取 3 | 4 | > 从图片中将中文文字提取下来。 5 | 6 | ## 开发人员 7 | 8 | WeChat: wgybzb 9 | 10 | QQ: 1010437118 11 | 12 | E-mail: wgybzb@sina.cn -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .project 2 | .classpath 3 | .metadata 4 | .settings 5 | target/ 6 | META-INF/ 7 | .DS_Store 8 | *.ipr 9 | *.iws 10 | .idea/ 11 | *.iml 12 | atlassian-ide-plugin.xml 13 | 14 | *.ppm 15 | *.mp4 16 | *.png 17 | *.csv 18 | *.yuv -------------------------------------------------------------------------------- /src/main/java/info/hb/lookup/object/common/GFirst.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object.common; 2 | 3 | import java.util.Comparator; 4 | 5 | public class GFirst implements Comparator { 6 | 7 | @Override 8 | public int compare(GPoint arg0, GPoint arg1) { 9 | int r = LessCompare.compareBigFirst(arg0.g, arg1.g); 10 | 11 | return r; 12 | } 13 | 14 | } -------------------------------------------------------------------------------- /src/main/java/info/hb/lookup/object/common/FontFamily.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object.common; 2 | 3 | import java.util.ArrayList; 4 | 5 | public class FontFamily extends ArrayList { 6 | 7 | private static final long serialVersionUID = 3279037448543102425L; 8 | 9 | public String name; 10 | 11 | public FontFamily(String name) { 12 | this.name = name; 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/info/hb/lookup/object/common/ImageBinary.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object.common; 2 | 3 | import java.awt.image.BufferedImage; 4 | import java.util.List; 5 | 6 | /** 7 | * Inteface for ImageBinaryGray and ImageBinaryRGB classes 8 | */ 9 | public interface ImageBinary { 10 | 11 | public List getChannels(); 12 | 13 | public int getWidth(); 14 | 15 | public int getHeight(); 16 | 17 | public int size(); 18 | 19 | public BufferedImage getImage(); 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/info/hb/lookup/object/common/GPoint.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object.common; 2 | 3 | import java.awt.Point; 4 | 5 | public class GPoint extends Point { 6 | 7 | private static final long serialVersionUID = -3174703028906427694L; 8 | 9 | public double g; 10 | 11 | public GPoint(int x, int y, double g) { 12 | super(x, y); 13 | 14 | this.g = g; 15 | } 16 | 17 | @Override 18 | public String toString() { 19 | return "[x=" + x + ",y=" + y + ",g=" + g + "]"; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/info/hb/lookup/object/common/ImageBinaryFeature.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object.common; 2 | 3 | import java.awt.image.BufferedImage; 4 | import java.util.List; 5 | 6 | /** 7 | * Inteface for ImageBinaryGrayFeature and ImageBinaryRGBFeature classes 8 | */ 9 | public interface ImageBinaryFeature { 10 | 11 | public List getFeatureChannels(); 12 | 13 | public int getWidth(); 14 | 15 | public int getHeight(); 16 | 17 | public int size(); 18 | 19 | public BufferedImage getImage(); 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/info/hb/lookup/object/common/FeatureSet.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object.common; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | public class FeatureSet extends ArrayList { 7 | 8 | private static final long serialVersionUID = -9077324933940287714L; 9 | 10 | public List k(IntegralImage template) { 11 | List list = new ArrayList(); 12 | 13 | for (Feature f : this) { 14 | list.add(new FeatureK(f, template)); 15 | } 16 | 17 | return list; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/test/java/info/hb/lookup/object/LookupTest.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object; 2 | 3 | import java.awt.Point; 4 | import java.awt.image.BufferedImage; 5 | import java.util.List; 6 | 7 | public class LookupTest { 8 | 9 | public static void main(String[] args) { 10 | BufferedImage image = Capture.load(OCRTest.class, "cyclopst1.png"); 11 | BufferedImage template = Capture.load(OCRTest.class, "cyclopst3.png"); 12 | 13 | List pp = LookupColor.lookupAll(image, template, 0.20f); 14 | 15 | for (Point p : pp) { 16 | System.out.println(p); 17 | } 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/info/hb/tesseract/example/TesseractDemo.java: -------------------------------------------------------------------------------- 1 | package info.hb.tesseract.example; 2 | 3 | import java.io.File; 4 | 5 | import net.sourceforge.tess4j.Tesseract; 6 | import net.sourceforge.tess4j.TesseractException; 7 | 8 | public class TesseractDemo { 9 | 10 | @SuppressWarnings("deprecation") 11 | public static void main(String[] args) { 12 | Tesseract instance = Tesseract.getInstance(); 13 | 14 | try { 15 | System.out.println(instance.doOCR(new File("image/test1.jpg"))); 16 | } catch (TesseractException e) { 17 | System.err.println(e.getMessage()); 18 | } 19 | 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/info/hb/lookup/object/common/GSPoint.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object.common; 2 | 3 | public class GSPoint extends GPoint { 4 | 5 | private static final long serialVersionUID = -3174703028906427694L; 6 | 7 | public double gg; 8 | 9 | public GSPoint(int x, int y, double gg, double g) { 10 | super(x, y, g); 11 | 12 | this.gg = gg; 13 | } 14 | 15 | public GSPoint(GPoint g, double gg) { 16 | super(g.x, g.y, g.g); 17 | 18 | this.gg = gg; 19 | } 20 | 21 | @Override 22 | public String toString() { 23 | return "[x=" + x + ",y=" + y + ",gg=" + gg + ",g=" + g + "]"; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/info/hb/lookup/object/common/ImageMultiplySum.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object.common; 2 | 3 | /** 4 | * Multiply matrix Pixel by Pixel and sum all pixels 5 | */ 6 | public class ImageMultiplySum { 7 | 8 | public ImageMultiply s = new ImageMultiply(); 9 | 10 | public double sum = 0; 11 | 12 | public ImageMultiplySum(SArray image, int xx, int yy, SArray template) { 13 | s.init(image, xx, yy, template); 14 | 15 | for (int x = 0; x < template.cx; x++) { 16 | for (int y = 0; y < template.cy; y++) { 17 | s.step(x, y); 18 | 19 | sum += s.s(x, y); 20 | } 21 | } 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/info/hb/lookup/object/common/IntegralImage.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object.common; 2 | 3 | /** 4 | * Sum Table. Integral sum. 5 | * 6 | * http://en.wikipedia.org/wiki/Summed_area_table 7 | */ 8 | public class IntegralImage extends SArray { 9 | 10 | public IntegralImage() { 11 | // 12 | } 13 | 14 | public IntegralImage(SArray buf) { 15 | initBase(buf); 16 | 17 | for (int x = 0; x < cx; x++) { 18 | for (int y = 0; y < cy; y++) { 19 | step(x, y); 20 | } 21 | } 22 | } 23 | 24 | public void step(int x, int y) { 25 | s(x, y, base.s(x, y) + s(x - 1, y) + s(x, y - 1) - s(x - 1, y - 1)); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/info/hb/lookup/object/common/FontSymbol.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object.common; 2 | 3 | import java.awt.image.BufferedImage; 4 | 5 | public class FontSymbol { 6 | 7 | public FontFamily fontFamily; 8 | public String fontSymbol; 9 | public ImageBinaryScale image; 10 | 11 | public FontSymbol(FontFamily ff, String fs, BufferedImage i) { 12 | this.fontFamily = ff; 13 | this.fontSymbol = fs; 14 | this.image = new ImageBinaryGreyScale(i); 15 | } 16 | 17 | public int getHeight() { 18 | return image.image.getHeight(); 19 | } 20 | 21 | public int getWidth() { 22 | return image.image.getWidth(); 23 | } 24 | 25 | @Override 26 | public String toString() { 27 | return fontSymbol; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/test/java/info/hb/lookup/object/SArrayTest.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object; 2 | 3 | import info.hb.lookup.object.common.SArray; 4 | 5 | public class SArrayTest { 6 | 7 | @SuppressWarnings("unused") 8 | public static void main(String[] args) { 9 | int[] s = new int[] { 10 | 11 | 5, 2, 5, 2, 12 | 13 | 3, 6, 3, 6, 14 | 15 | 5, 2, 5, 2, 16 | 17 | 3, 6, 3, 6 18 | // 19 | }; 20 | 21 | SArray a = new SArray(); 22 | a.cx = 4; 23 | a.cy = 4; 24 | a.s = new double[a.cx * a.cy]; 25 | 26 | for (int y = 0; y < a.cy; y++) { 27 | for (int x = 0; x < a.cx; x++) { 28 | a.s(x, y, a.s(x, y) + a.s(x - 1, y) + a.s(x, y - 1) - a.s(x - 1, y - 1)); 29 | } 30 | } 31 | 32 | boolean t = a.sigma() == 64; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/info/hb/lookup/object/common/ImageZeroMean.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object.common; 2 | 3 | /** 4 | * Zero Mean Image is an image where every pixel in the image substracted by 5 | * mean value of the image. Mean value of the image is sum of all pixels values 6 | * devided by number of pixels. 7 | */ 8 | public class ImageZeroMean extends SArray { 9 | 10 | double m; 11 | 12 | public ImageZeroMean() { 13 | // 14 | } 15 | 16 | public ImageZeroMean(SArray s1) { 17 | init(s1); 18 | 19 | for (int x = 0; x < cx; x++) { 20 | for (int y = 0; y < cy; y++) { 21 | step(x, y); 22 | } 23 | } 24 | } 25 | 26 | public void init(SArray s1) { 27 | base = s1.base; 28 | 29 | cx = s1.cx; 30 | cy = s1.cy; 31 | 32 | s = new double[cx * cy]; 33 | 34 | m = s1.mean(); 35 | } 36 | 37 | public void step(int x, int y) { 38 | s(x, y, base.s(x, y) - m); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/info/hb/lookup/object/common/Feature.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object.common; 2 | 3 | import java.awt.image.BufferedImage; 4 | 5 | /** 6 | * Haar Like feature 7 | */ 8 | public class Feature extends SArray { 9 | 10 | public Feature(int cx, int cy, double[] i) { 11 | this.cx = cx; 12 | this.cy = cy; 13 | this.s = i; 14 | } 15 | 16 | public Feature(SArray s) { 17 | this.cx = s.cx; 18 | this.cy = s.cy; 19 | this.s = s.s; 20 | } 21 | 22 | @Override 23 | public BufferedImage getImage() { 24 | int[] ss = new int[s.length]; 25 | for (int i = 0; i < ss.length; i++) { 26 | int argb = s[i] == 1 ? 0xffffffff : 0xff000000; 27 | 28 | ss[i] = argb; 29 | } 30 | 31 | BufferedImage image = new BufferedImage(cx, cy, BufferedImage.TYPE_INT_ARGB); 32 | image.getWritableTile(0, 0).setDataElements(0, 0, image.getWidth(), image.getHeight(), ss); 33 | return image; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/info/hb/lookup/object/common/ImageMultiply.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object.common; 2 | 3 | /** 4 | * Multiply matrix pixel by pixel 5 | */ 6 | public class ImageMultiply extends SArray { 7 | 8 | public SArray m; 9 | public int xx; 10 | public int yy; 11 | 12 | public ImageMultiply() { 13 | // 14 | } 15 | 16 | public ImageMultiply(SArray s1, SArray s2) { 17 | this(s1, 0, 0, s2); 18 | } 19 | 20 | public ImageMultiply(SArray image, int xx, int yy, SArray template) { 21 | init(image, xx, yy, template); 22 | 23 | for (int x = 0; x < cx; x++) { 24 | for (int y = 0; y < cy; y++) { 25 | step(x, y); 26 | } 27 | } 28 | } 29 | 30 | public void init(SArray image, int xx, int yy, SArray template) { 31 | super.initBase(template); 32 | 33 | this.m = image; 34 | this.xx = xx; 35 | this.yy = yy; 36 | } 37 | 38 | public void step(int x, int y) { 39 | s(x, y, m.s(xx + x, yy + y) * base.s(x, y)); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/info/hb/lookup/object/common/ImageBinaryGreyScale.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object.common; 2 | 3 | import java.awt.image.BufferedImage; 4 | 5 | public class ImageBinaryGreyScale extends ImageBinaryScale { 6 | 7 | public ImageBinaryGreyScale(BufferedImage i) { 8 | image = new ImageBinaryGrey(i); 9 | s = 1f; 10 | k = 0; 11 | scales.add(image); 12 | } 13 | 14 | /** 15 | * @param i 16 | * @param scaleSize 17 | * template scale size in pixels you wish. (ex: 5) 18 | */ 19 | public ImageBinaryGreyScale(BufferedImage i, int scaleSize, int blurKernel) { 20 | image = new ImageBinaryGrey(i); 21 | 22 | rescale(scaleSize, blurKernel); 23 | } 24 | 25 | public ImageBinaryGreyScale(BufferedImage i, double scale, int blurKernel) { 26 | image = new ImageBinaryGrey(i); 27 | 28 | rescale(scale, blurKernel); 29 | } 30 | 31 | @Override 32 | public ImageBinary rescale(BufferedImage i) { 33 | return new ImageBinaryGrey(i); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/info/hb/lookup/object/common/ImageBinaryGreyScaleRGB.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object.common; 2 | 3 | import java.awt.image.BufferedImage; 4 | 5 | /** 6 | * mix RGB and Grey for memory saving 7 | * 8 | * Scaled image is RGB and Full Scale is Gray 9 | */ 10 | public class ImageBinaryGreyScaleRGB extends ImageBinaryScale { 11 | 12 | public ImageBinaryGreyScaleRGB(BufferedImage i) { 13 | image = new ImageBinaryGrey(i); 14 | } 15 | 16 | /** 17 | * 18 | * @param i 19 | * @param scaleSize 20 | * template scale size in pixels you wish. (ex: 5) 21 | */ 22 | public ImageBinaryGreyScaleRGB(BufferedImage i, int scaleSize, int blurKernel) { 23 | image = new ImageBinaryRGB(i); 24 | 25 | rescale(scaleSize, blurKernel); 26 | } 27 | 28 | public ImageBinaryGreyScaleRGB(BufferedImage i, double scale, int blurKernel) { 29 | image = new ImageBinaryRGB(i); 30 | 31 | rescale(scale, blurKernel); 32 | } 33 | 34 | @Override 35 | public ImageBinary rescale(BufferedImage i) { 36 | return new ImageBinaryRGB(i); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/test/java/info/hb/lookup/object/NCCTest.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object; 2 | 3 | import info.hb.lookup.object.common.GPoint; 4 | import info.hb.lookup.object.common.ImageBinaryGrey; 5 | import info.hb.lookup.object.common.ImageBinaryRGB; 6 | import info.hb.lookup.object.proc.NCC; 7 | 8 | import java.awt.image.BufferedImage; 9 | import java.util.List; 10 | 11 | public class NCCTest { 12 | 13 | public static void main(String[] args) { 14 | BufferedImage image = Capture.load(OCRTest.class, "cyclopst1.png"); 15 | BufferedImage template = Capture.load(OCRTest.class, "cyclopst3.png"); 16 | 17 | // rgb image lookup 18 | { 19 | List pp = NCC.lookupAll(new ImageBinaryRGB(image), new ImageBinaryRGB(template), 0.9f); 20 | 21 | for (GPoint p : pp) { 22 | System.out.println(p); 23 | } 24 | } 25 | 26 | // grey image lookup 27 | { 28 | List pp = NCC.lookupAll(new ImageBinaryGrey(image), new ImageBinaryGrey(template), 0.9f); 29 | 30 | for (GPoint p : pp) { 31 | System.out.println(p); 32 | } 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/info/hb/lookup/object/common/GFirstLeftRight.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object.common; 2 | 3 | import java.awt.Rectangle; 4 | import java.util.Comparator; 5 | 6 | public class GFirstLeftRight implements Comparator { 7 | 8 | public ImageBinary image; 9 | public int wh; 10 | public int hh; 11 | 12 | public GFirstLeftRight(ImageBinary image) { 13 | this.image = image; 14 | wh = image.getWidth() / 2; 15 | hh = image.getHeight() / 2; 16 | } 17 | 18 | @Override 19 | public int compare(GPoint arg0, GPoint arg1) { 20 | int r = 0; 21 | 22 | Rectangle r1 = new Rectangle(arg0.x - wh, arg0.y - hh, image.getWidth(), image.getHeight()); 23 | Rectangle r2 = new Rectangle(arg1.x - wh, arg1.y - hh, image.getWidth(), image.getHeight()); 24 | 25 | if (!r1.intersects(r2)) { 26 | if (r == 0) 27 | r = LessCompare.compareSmallFirst(arg0.y, arg1.y); 28 | if (r == 0) 29 | r = LessCompare.compareSmallFirst(arg0.x, arg1.x); 30 | } 31 | 32 | if (r == 0) 33 | r = LessCompare.compareBigFirst(arg0.g, arg1.g); 34 | 35 | return r; 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /src/main/java/info/hb/lookup/object/common/GrayImage.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object.common; 2 | 3 | import java.awt.image.BufferedImage; 4 | 5 | /** 6 | * BufferedImage to Gray array converter. 7 | */ 8 | public class GrayImage extends SArray { 9 | 10 | public BufferedImage buf; 11 | 12 | public int grey(int x, int y) { 13 | int argb = buf.getRGB(x, y); 14 | 15 | // int a = (argb & 0xff000000) >> 24; 16 | int r = (argb & 0x00ff0000) >> 16; 17 | int g = (argb & 0x0000ff00) >> 8; 18 | int b = (argb & 0x000000ff); 19 | 20 | return (r + g + b) / 3; 21 | } 22 | 23 | public GrayImage() { 24 | // 25 | } 26 | 27 | public GrayImage(BufferedImage buf) { 28 | init(buf); 29 | 30 | for (int x = 0; x < cx; x++) { 31 | for (int y = 0; y < cy; y++) { 32 | step(x, y); 33 | } 34 | } 35 | } 36 | 37 | public void init(BufferedImage buf) { 38 | this.buf = buf; 39 | 40 | cx = buf.getWidth(); 41 | cy = buf.getHeight(); 42 | 43 | s = new double[cx * cy]; 44 | } 45 | 46 | public void step(int x, int y) { 47 | s(x, y, grey(x, y)); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/info/hb/lookup/object/common/ImageBinaryChannelFeature.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object.common; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * Same as ImageBinaryChannel but with addition of Integral Image over Zero Mean 7 | * Image matrix. 8 | */ 9 | public class ImageBinaryChannelFeature extends ImageBinaryChannel { 10 | 11 | public List k; 12 | 13 | public IntegralImage zeroMeanIntegral; 14 | 15 | public ImageBinaryChannelFeature(ChannelType t) { 16 | super(t); 17 | integral = new IntegralImage(); 18 | integral2 = new IntegralImage2(); 19 | } 20 | 21 | public ImageBinaryChannelFeature(ChannelType t, SArray template, FeatureSet list) { 22 | super(t, template); 23 | 24 | zeroMeanIntegral = new IntegralImage(zeroMean); 25 | 26 | init(list); 27 | } 28 | 29 | public ImageBinaryChannelFeature(ChannelType t, SArray template, double threshold) { 30 | super(t, template); 31 | 32 | zeroMeanIntegral = new IntegralImage(zeroMean); 33 | 34 | FeatureSet list = new FeatureSetAuto(this, threshold); 35 | 36 | init(list); 37 | } 38 | 39 | void init(FeatureSet list) { 40 | k = list.k(zeroMeanIntegral); 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /src/test/java/info/hb/lookup/object/FNCCTest.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object; 2 | 3 | import info.hb.lookup.object.common.GPoint; 4 | import info.hb.lookup.object.common.ImageBinaryGrey; 5 | import info.hb.lookup.object.common.ImageBinaryRGB; 6 | import info.hb.lookup.object.common.ImageBinaryRGBFeature; 7 | import info.hb.lookup.object.proc.FNCC; 8 | 9 | import java.awt.image.BufferedImage; 10 | import java.util.List; 11 | 12 | public class FNCCTest { 13 | 14 | public static void main(String[] args) { 15 | BufferedImage image = Capture.load(OCRTest.class, "cyclopst1.png"); 16 | BufferedImage template = Capture.load(OCRTest.class, "cyclopst3.png"); 17 | 18 | // rgb image lookup 19 | { 20 | List pp = FNCC 21 | .lookupAll(new ImageBinaryRGB(image), new ImageBinaryRGBFeature(template, 5000), 0.9f); 22 | 23 | for (GPoint p : pp) { 24 | System.out.println(p); 25 | } 26 | } 27 | 28 | // grey image lookup 29 | { 30 | List pp = FNCC.lookupAll(new ImageBinaryGrey(image), new ImageBinaryRGBFeature(template, 5000), 31 | 0.9f); 32 | 33 | for (GPoint p : pp) { 34 | System.out.println(p); 35 | } 36 | } 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/info/hb/lookup/object/common/FontSymbolLookup.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object.common; 2 | 3 | import java.awt.Rectangle; 4 | 5 | import org.apache.commons.lang.math.IntRange; 6 | 7 | public class FontSymbolLookup { 8 | 9 | public int x; 10 | public int y; 11 | public FontSymbol fs; 12 | public double g; 13 | 14 | public FontSymbolLookup(FontSymbol fs, int x, int y, double g) { 15 | this.fs = fs; 16 | this.x = x; 17 | this.y = y; 18 | this.g = g; 19 | } 20 | 21 | public int size() { 22 | return fs.getHeight() * fs.getWidth(); 23 | } 24 | 25 | public boolean cross(FontSymbolLookup f) { 26 | Rectangle r = new Rectangle(x, y, fs.getWidth(), fs.getHeight()); 27 | Rectangle r2 = new Rectangle(f.x, f.y, f.fs.getWidth(), f.fs.getHeight()); 28 | 29 | return r.intersects(r2); 30 | } 31 | 32 | public boolean yCross(FontSymbolLookup f) { 33 | IntRange r1 = new IntRange(y, y + fs.getHeight()); 34 | 35 | IntRange r2 = new IntRange(f.y, f.y + f.fs.getHeight()); 36 | 37 | return r1.overlapsRange(r2); 38 | } 39 | 40 | public int getWidth() { 41 | return fs.getWidth(); 42 | } 43 | 44 | public int getHeight() { 45 | return fs.getHeight(); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/info/hb/lookup/object/common/RGBImage.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object.common; 2 | 3 | import java.awt.image.BufferedImage; 4 | 5 | /** 6 | * BufferedImage to 3 arrays converter (rgb arrays) 7 | */ 8 | public class RGBImage { 9 | 10 | public BufferedImage buf; 11 | 12 | public int cx; 13 | public int cy; 14 | 15 | public SArray r; 16 | public SArray g; 17 | public SArray b; 18 | 19 | public RGBImage() { 20 | // 21 | } 22 | 23 | public RGBImage(BufferedImage buf) { 24 | init(buf); 25 | 26 | for (int x = 0; x < cx; x++) { 27 | for (int y = 0; y < cy; y++) { 28 | step(x, y); 29 | } 30 | } 31 | } 32 | 33 | public void init(BufferedImage buf) { 34 | this.buf = buf; 35 | 36 | cx = buf.getWidth(); 37 | cy = buf.getHeight(); 38 | 39 | r = new SArray(cx, cy); 40 | g = new SArray(cx, cy); 41 | b = new SArray(cx, cy); 42 | } 43 | 44 | public void step(int x, int y) { 45 | int argb = buf.getRGB(x, y); 46 | 47 | // int a = (argb & 0xff000000) >> 24; 48 | int r = (argb & 0x00ff0000) >> 16; 49 | int g = (argb & 0x0000ff00) >> 8; 50 | int b = (argb & 0x000000ff); 51 | 52 | this.r.s(x, y, r); 53 | this.g.s(x, y, g); 54 | this.b.s(x, y, b); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/test/java/info/hb/lookup/object/OCRTest.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object; 2 | 3 | import info.hb.lookup.object.common.ImageBinaryGrey; 4 | 5 | import java.io.File; 6 | 7 | public class OCRTest { 8 | 9 | static public void main(String[] args) { 10 | OCR l = new OCR(0.70f); 11 | 12 | // will go to com/github/axet/lookup/fonts folder and load all font 13 | // familys (here is only font_1 family in this library) 14 | l.loadFontsDirectory(OCRTest.class, new File("fonts")); 15 | 16 | // example how to load only one family 17 | // "com/github/axet/lookup/fonts/font_1" 18 | l.loadFont(OCRTest.class, new File("fonts", "font_1")); 19 | 20 | String str = ""; 21 | 22 | // recognize using all familys set 23 | str = l.recognize(Capture.load(OCRTest.class, "test3.png")); 24 | System.out.println(str); 25 | 26 | // recognize using only one family set 27 | str = l.recognize(Capture.load(OCRTest.class, "test3.png"), "font_1"); 28 | System.out.println(str); 29 | 30 | // recognize using only one family set and rectangle 31 | ImageBinaryGrey i = new ImageBinaryGrey(Capture.load(OCRTest.class, "full.png")); 32 | str = l.recognize(i, 1285, 654, 1343, 677, "font_1"); 33 | System.out.println(str); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/info/hb/tesseract/example/Demo1.java: -------------------------------------------------------------------------------- 1 | package info.hb.tesseract.example; 2 | 3 | 4 | public class Demo1 { 5 | 6 | public static void main(String[] args) { 7 | // TessBaseAPI api = TessAPI1.TessBaseAPICreate(); 8 | // TessAPI1.TessBaseAPIInit3(api, path, lang); 9 | // TessAPI1.TessBaseAPISetPageSegMode(api, TessAPI1.TessPageSegMode.PSM_AUTO); 10 | // TessAPI1.TessBaseAPISetImage(api, img, w, h, bpp, bpp * w); 11 | // TessAPI1.TessBaseAPIGetUTF8Text(api); 12 | // TessResultIterator it = TessAPI1.TessBaseAPIGetIterator(api); 13 | 14 | // TessResultIterator ri = TessAPI1.TessBaseAPIGetIterator(api); 15 | // TessPageIterator pi = TessAPI1.TessResultIteratorGetPageIterator(ri); 16 | // Pointer str = TessAPI1.TessResultIteratorGetUTF8Text(ri, TessPageIteratorLevel.RIL_WORD); 17 | // IntBuffer leftB = IntBuffer.allocate(1); 18 | // IntBuffer topB = IntBuffer.allocate(1); 19 | // IntBuffer rightB = IntBuffer.allocate(1); 20 | // IntBuffer bottomB = IntBuffer.allocate(1); 21 | // TessAPI1.TessPageIteratorBoundingBox(pi, TessPageIteratorLevel.RIL_WORD, leftB, topB, rightB, bottomB); 22 | // int left = leftB.get(); 23 | // int top = topB.get(); 24 | // int right = rightB.get(); 25 | // int bottom = bottomB.get(); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/test/java/info/hb/lookup/object/SNCCTest.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object; 2 | 3 | import info.hb.lookup.object.common.GFirst; 4 | import info.hb.lookup.object.common.GPoint; 5 | import info.hb.lookup.object.common.GSPoint; 6 | import info.hb.lookup.object.common.ImageBinaryGreyScaleRGB; 7 | import info.hb.lookup.object.common.ImageBinaryScale; 8 | 9 | import java.awt.image.BufferedImage; 10 | import java.util.Collections; 11 | import java.util.List; 12 | 13 | public class SNCCTest { 14 | 15 | public static void main(String[] args) { 16 | BufferedImage image = Capture.load(OCRTest.class, "desktop.png"); 17 | BufferedImage template = Capture.load(OCRTest.class, "desktop_feature_big.png"); 18 | 19 | LookupScale s = new LookupScale(0.2f, 10, 0.60f, 0.9f); 20 | 21 | ImageBinaryScale si = new ImageBinaryGreyScaleRGB(image); 22 | 23 | ImageBinaryScale st = new ImageBinaryGreyScaleRGB(template); 24 | 25 | Long l; 26 | 27 | for (int i = 0; i < 2; i++) { 28 | l = System.currentTimeMillis(); 29 | { 30 | List pp = s.lookupAll(si, st); 31 | 32 | Collections.sort(pp, new GFirst()); 33 | 34 | for (GPoint p : pp) { 35 | System.out.println(p); 36 | } 37 | } 38 | System.out.println(System.currentTimeMillis() - l); 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/test/java/info/hb/lookup/object/OCRScaleTest.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object; 2 | 3 | import info.hb.lookup.object.common.ImageBinaryGreyScale; 4 | 5 | import java.io.File; 6 | 7 | public class OCRScaleTest { 8 | 9 | static public void main(String[] args) { 10 | OCRScale l = new OCRScale(0.5f, 3, 0.70f); 11 | 12 | // will go to com/github/axet/lookup/fonts folder and load all font 13 | // familys (here is only font_1 family in this library) 14 | l.loadFontsDirectory(OCRScaleTest.class, new File("fonts")); 15 | 16 | // example how to load only one family 17 | // "com/github/axet/lookup/fonts/font_1" 18 | l.loadFont(OCRScaleTest.class, new File("fonts", "font_1")); 19 | 20 | String str = ""; 21 | 22 | // recognize using all familys set 23 | str = l.recognize(Capture.load(OCRScaleTest.class, "test3.png")); 24 | System.out.println(str); 25 | 26 | // recognize using only one family set 27 | str = l.recognize(Capture.load(OCRScaleTest.class, "test3.png"), "font_1"); 28 | System.out.println(str); 29 | 30 | // recognize using only one family set and rectangle 31 | ImageBinaryGreyScale i = new ImageBinaryGreyScale(Capture.load(OCRScaleTest.class, "full.png")); 32 | str = l.recognize(i, 1285, 654, 1343, 677, "font_1"); 33 | System.out.println(str); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/info/hb/ocr/example/ImageScanner.java: -------------------------------------------------------------------------------- 1 | package info.hb.ocr.example; 2 | 3 | import java.awt.Image; 4 | import java.io.File; 5 | import java.util.ArrayList; 6 | import java.util.HashMap; 7 | 8 | import javax.imageio.ImageIO; 9 | 10 | import net.sourceforge.javaocr.ocrPlugins.mseOCR.CharacterRange; 11 | import net.sourceforge.javaocr.ocrPlugins.mseOCR.OCRScanner; 12 | import net.sourceforge.javaocr.ocrPlugins.mseOCR.TrainingImage; 13 | import net.sourceforge.javaocr.ocrPlugins.mseOCR.TrainingImageLoader; 14 | import net.sourceforge.javaocr.scanner.PixelImage; 15 | 16 | @SuppressWarnings("deprecation") 17 | public class ImageScanner { 18 | 19 | public static void main(String[] args) throws Exception { 20 | OCRScanner scanner = new OCRScanner(); 21 | TrainingImageLoader loader = new TrainingImageLoader(); 22 | HashMap> trainingImageMap = new HashMap<>(); 23 | loader.load("image/test4.png", new CharacterRange('!', '~'), trainingImageMap); 24 | scanner.addTrainingImages(trainingImageMap); 25 | 26 | Image image = ImageIO.read(new File("image/test4.png")); 27 | PixelImage pixelImage = new PixelImage(image); 28 | pixelImage.toGrayScale(true); 29 | pixelImage.filter(); 30 | 31 | String text = scanner.scan(image, 0, 0, 0, 0, null); 32 | System.out.println(text); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/info/hb/lookup/object/common/FeatureSetAuto.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object.common; 2 | 3 | public class FeatureSetAuto extends FeatureSet { 4 | 5 | private static final long serialVersionUID = -4442575077693435100L; 6 | 7 | public FeatureSetAuto(ImageBinaryChannelFeature template, double threshold) { 8 | 9 | Feature f = new Feature(1, 1, new double[] { 10 | 11 | 1, 12 | 13 | }); 14 | 15 | FeatureK k = new FeatureK(f, template.zeroMeanIntegral); 16 | 17 | FeatureSet s = j(template, k, threshold); 18 | 19 | addAll(s); 20 | } 21 | 22 | public FeatureSet j(ImageBinaryChannelFeature template, FeatureK k, double threshold) { 23 | FeatureSet list = new FeatureSet(); 24 | 25 | for (RectK r : k.list) { 26 | list.addAll(j(template, threshold, r)); 27 | } 28 | 29 | return list; 30 | } 31 | 32 | public FeatureSet j(ImageBinaryChannelFeature template, double threshold, RectK r) { 33 | 34 | FeatureSet s = new FeatureSet(); 35 | 36 | double j = 0; 37 | 38 | for (int x = r.x1; x <= r.x2; x++) { 39 | for (int y = r.y1; y <= r.y2; y++) { 40 | j += IntegralImage2.pow2(template.zeroMean.s(x, y) - r.k); 41 | } 42 | } 43 | 44 | if (j > threshold) { 45 | RectK[] rr = r.devide(); 46 | if (rr == null) { 47 | s.add(r.getFeature()); 48 | } else { 49 | for (RectK rrr : rr) { 50 | s.addAll(j(template, threshold, rrr)); 51 | } 52 | } 53 | } else { 54 | s.add(r.getFeature()); 55 | } 56 | 57 | return s; 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/info/hb/lookup/object/common/ImageBinaryGrey.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object.common; 2 | 3 | import info.hb.lookup.object.common.ImageBinaryChannel.ChannelType; 4 | 5 | import java.awt.image.BufferedImage; 6 | import java.util.Arrays; 7 | import java.util.List; 8 | 9 | /** 10 | * Container for ImageBinary class for each channel (one gray channel here) 11 | */ 12 | public class ImageBinaryGrey implements ImageBinary { 13 | 14 | public GrayImage gi; 15 | public ImageBinaryChannel gray; 16 | 17 | public List list; 18 | 19 | public ImageBinaryGrey(BufferedImage img) { 20 | gi = new GrayImage(); 21 | gray = new ImageBinaryChannel(ChannelType.GREY); 22 | 23 | list = Arrays.asList(new ImageBinaryChannel[] { gray }); 24 | 25 | this.gi.init(img); 26 | this.gray.initBase(gi); 27 | 28 | for (int x = 0; x < this.gi.cx; x++) { 29 | for (int y = 0; y < this.gi.cy; y++) { 30 | this.gi.step(x, y); 31 | this.gray.step(x, y); 32 | } 33 | } 34 | 35 | gray.zeroMean = new ImageZeroMean(gray.integral); 36 | } 37 | 38 | @Override 39 | public int getWidth() { 40 | return gi.cx; 41 | } 42 | 43 | @Override 44 | public int getHeight() { 45 | return gi.cy; 46 | } 47 | 48 | @Override 49 | public int size() { 50 | return gi.cx * gi.cy; 51 | } 52 | 53 | @Override 54 | public BufferedImage getImage() { 55 | return gi.buf; 56 | } 57 | 58 | @Override 59 | public List getChannels() { 60 | return list; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/info/hb/lookup/object/common/FeatureSetDefault.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object.common; 2 | 3 | public class FeatureSetDefault extends FeatureSet { 4 | 5 | private static final long serialVersionUID = -4442575077693435100L; 6 | 7 | public FeatureSetDefault() { 8 | 9 | add(new Feature(1, 1, new double[] { 10 | 11 | 1, 12 | 13 | })); 14 | // 15 | // add(new Feature(3, 3, new double[] { 16 | // 17 | // 0, 0, 0, 18 | // 19 | // 0, 1, 0, 20 | // 21 | // 0, 0, 0 22 | // 23 | // })); 24 | // 25 | // add(new Feature(2, 2, new double[] { 26 | // 27 | // 0, 1, 28 | // 29 | // 0, 1, 30 | // 31 | // })); 32 | // 33 | // add(new Feature(2, 2, new double[] { 34 | // 35 | // 1, 0, 36 | // 37 | // 1, 0, 38 | // 39 | // })); 40 | // 41 | // add(new Feature(2, 2, new double[] { 42 | // 43 | // 1, 1, 44 | // 45 | // 0, 0, 46 | // 47 | // })); 48 | // 49 | // add(new Feature(2, 2, new double[] { 50 | // 51 | // 0, 0, 52 | // 53 | // 1, 1, 54 | // 55 | // })); 56 | // 57 | // add(new Feature(2, 3, new double[] { 58 | // 59 | // 0, 0, 60 | // 61 | // 1, 1, 62 | // 63 | // 0, 0, 64 | // 65 | // })); 66 | // 67 | // add(new Feature(3, 3, new double[] { 68 | // 69 | // 0, 0, 0, 70 | // 71 | // 1, 1, 1, 72 | // 73 | // 0, 0, 0, 74 | // 75 | // })); 76 | 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/info/hb/lookup/object/common/LessCompare.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object.common; 2 | 3 | /** 4 | * Java Comparator has compare method with 'int compare(o1, o2)' result value. 5 | * You should read ita as less algorithm, who is less? 6 | * 7 | * o1 < o2 8 | * 9 | * -1 points to low value for o1. 0 points for middle / equals. 1 points for o2 10 | * low value 11 | * 12 | * if you need opposite (desk order) return opposite values. 13 | */ 14 | public class LessCompare { 15 | 16 | static public int compareBigFirst(double o1, double o2, double val) { 17 | if (Math.abs(Math.abs(o1) - Math.abs(o2)) < val) 18 | return 0; 19 | 20 | return compareBigFirst(o1, o2); 21 | } 22 | 23 | // desc algorithm (high comes at first [0]) 24 | static public int compareBigFirst(int o1, int o2) { 25 | return new Integer(o2).compareTo(new Integer(o1)); 26 | } 27 | 28 | // desc algorithm (high comes at first [0]) 29 | static public int compareBigFirst(float o1, float o2) { 30 | return new Float(o2).compareTo(new Float(o1)); 31 | } 32 | 33 | static public int compareBigFirst(double o1, double o2) { 34 | return new Double(o2).compareTo(new Double(o1)); 35 | } 36 | 37 | // asc algorithm (low comes at first [0]) 38 | static public int compareSmallFirst(int o1, int o2) { 39 | return new Integer(o1).compareTo(new Integer(o2)); 40 | } 41 | 42 | static public int compareBigFirst(int o1, int o2, int val) { 43 | if (Math.abs(Math.abs(o1) - Math.abs(o2)) < val) 44 | return 0; 45 | 46 | return compareBigFirst(o1, o2); 47 | } 48 | 49 | static public int compareSmallFirst(double o1, double o2) { 50 | return new Double(o1).compareTo(new Double(o2)); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/info/hb/lookup/object/common/IntegralImage2.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object.common; 2 | 3 | /** 4 | * Image Energy. Squared Image Function f^2(x,y). 5 | * 6 | * http://en.wikipedia.org/wiki/Summed_area_table 7 | */ 8 | public class IntegralImage2 extends SArray { 9 | 10 | static public double pow2(double x) { 11 | return x * x; 12 | } 13 | 14 | public IntegralImage2() { 15 | } 16 | 17 | public IntegralImage2(SArray buf) { 18 | initBase(buf); 19 | 20 | for (int x = 0; x < cx; x++) { 21 | for (int y = 0; y < cy; y++) { 22 | step(x, y); 23 | } 24 | } 25 | } 26 | 27 | public void step(int x, int y) { 28 | s(x, y, pow2(base.s(x, y)) + s(x - 1, y) + s(x, y - 1) - s(x - 1, y - 1)); 29 | } 30 | 31 | /** 32 | * Standard deviation no sqrt and no mean 33 | * 34 | * @param i 35 | * @param x1 36 | * @param y1 37 | * @param x2 38 | * @param y2 39 | * @return 40 | */ 41 | 42 | public double dev2n(IntegralImage i, int x1, int y1, int x2, int y2) { 43 | double sum = i.sigma(x1, y1, x2, y2); 44 | int size = (x2 - x1 + 1) * (y2 - y1 + 1); 45 | double sum2 = sigma(x1, y1, x2, y2); 46 | return sum2 - pow2(sum) / size; 47 | } 48 | 49 | /** 50 | * Standard deviation no sqrt 51 | * 52 | * @param i 53 | * @param x1 54 | * @param y1 55 | * @param x2 56 | * @param y2 57 | * @return 58 | */ 59 | public double dev2(IntegralImage i, int x1, int y1, int x2, int y2) { 60 | int size = (x2 - x1 + 1) * (y2 - y1 + 1); 61 | return dev2n(i, x1, y1, x2, y2) / (size - 1); 62 | } 63 | 64 | /** 65 | * Standard deviation 66 | * 67 | * @param i 68 | * @param x1 69 | * @param y1 70 | * @param x2 71 | * @param y2 72 | * @return 73 | */ 74 | public double dev(IntegralImage i, int x1, int y1, int x2, int y2) { 75 | return Math.sqrt(dev2(i, x1, y1, x2, y2)); 76 | } 77 | 78 | public double dev2n(IntegralImage i) { 79 | return dev2n(i, 0, 0, cx - 1, cy - 1); 80 | } 81 | 82 | public double dev2(IntegralImage i) { 83 | return dev2(i, 0, 0, cx - 1, cy - 1); 84 | } 85 | 86 | public double dev(IntegralImage i) { 87 | return dev(i, 0, 0, cx - 1, cy - 1); 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/info/hb/lookup/object/common/ImageBinaryRGB.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object.common; 2 | 3 | import info.hb.lookup.object.common.ImageBinaryChannel.ChannelType; 4 | 5 | import java.awt.image.BufferedImage; 6 | import java.util.Arrays; 7 | import java.util.List; 8 | 9 | /** 10 | * Container for ImageBinary class for each channel (rgb channels here) 11 | */ 12 | public class ImageBinaryRGB implements ImageBinary { 13 | 14 | public RGBImage image; 15 | public ImageBinaryChannel r; 16 | public ImageBinaryChannel g; 17 | public ImageBinaryChannel b; 18 | 19 | public List list; 20 | 21 | public ImageBinaryRGB(BufferedImage img) { 22 | image = new RGBImage(); 23 | r = new ImageBinaryChannel(ChannelType.RED); 24 | g = new ImageBinaryChannel(ChannelType.GREEN); 25 | b = new ImageBinaryChannel(ChannelType.BLUE); 26 | 27 | list = Arrays.asList(new ImageBinaryChannel[] { r, g, b }); 28 | 29 | this.image.init(img); 30 | this.r.initBase(this.image.r); 31 | this.g.initBase(this.image.g); 32 | this.b.initBase(this.image.b); 33 | 34 | for (int x = 0; x < this.image.cx; x++) { 35 | for (int y = 0; y < this.image.cy; y++) { 36 | this.image.step(x, y); 37 | this.r.step(x, y); 38 | this.g.step(x, y); 39 | this.b.step(x, y); 40 | } 41 | } 42 | 43 | r.zeroMean = new ImageZeroMean(); 44 | g.zeroMean = new ImageZeroMean(); 45 | b.zeroMean = new ImageZeroMean(); 46 | r.zeroMean.init(r.integral); 47 | g.zeroMean.init(g.integral); 48 | b.zeroMean.init(b.integral); 49 | 50 | for (int x = 0; x < this.image.cx; x++) { 51 | for (int y = 0; y < this.image.cy; y++) { 52 | r.zeroMean.step(x, y); 53 | g.zeroMean.step(x, y); 54 | b.zeroMean.step(x, y); 55 | } 56 | } 57 | } 58 | 59 | @Override 60 | public int getWidth() { 61 | return image.cx; 62 | } 63 | 64 | @Override 65 | public int getHeight() { 66 | return image.cy; 67 | } 68 | 69 | @Override 70 | public int size() { 71 | return image.cx * image.cy; 72 | } 73 | 74 | @Override 75 | public BufferedImage getImage() { 76 | return image.buf; 77 | } 78 | 79 | @Override 80 | public List getChannels() { 81 | return list; 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/info/hb/lookup/object/common/ImageBinaryGreyFeature.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object.common; 2 | 3 | import info.hb.lookup.object.common.ImageBinaryChannel.ChannelType; 4 | 5 | import java.awt.image.BufferedImage; 6 | import java.util.Arrays; 7 | import java.util.List; 8 | 9 | /** 10 | * Container for ImageBinary and Feature class for each channel (one gray channel here) 11 | */ 12 | public class ImageBinaryGreyFeature implements ImageBinaryFeature { 13 | 14 | public RGBImage image; 15 | public ImageBinaryChannelFeature grey; 16 | 17 | public List list; 18 | 19 | public ImageBinaryGreyFeature(BufferedImage img, double threshold) { 20 | init(img); 21 | 22 | FeatureSet lr = new FeatureSetAuto(grey, threshold); 23 | grey.init(lr); 24 | 25 | } 26 | 27 | public void init(BufferedImage img) { 28 | image = new RGBImage(); 29 | grey = new ImageBinaryChannelFeature(ChannelType.GREY); 30 | 31 | list = Arrays.asList(new ImageBinaryChannelFeature[] { grey }); 32 | 33 | this.image.init(img); 34 | this.grey.initBase(this.image.r); 35 | 36 | for (int x = 0; x < this.image.cx; x++) { 37 | for (int y = 0; y < this.image.cy; y++) { 38 | this.image.step(x, y); 39 | this.grey.step(x, y); 40 | } 41 | } 42 | 43 | grey.zeroMean = new ImageZeroMean(); 44 | grey.zeroMean.init(grey.integral); 45 | 46 | for (int x = 0; x < this.image.cx; x++) { 47 | for (int y = 0; y < this.image.cy; y++) { 48 | grey.zeroMean.step(x, y); 49 | } 50 | } 51 | 52 | grey.zeroMeanIntegral = new IntegralImage(); 53 | grey.zeroMeanIntegral.initBase(grey.zeroMean); 54 | 55 | for (int x = 0; x < this.image.cx; x++) { 56 | for (int y = 0; y < this.image.cy; y++) { 57 | grey.zeroMeanIntegral.step(x, y); 58 | } 59 | } 60 | } 61 | 62 | @Override 63 | public int getWidth() { 64 | return image.cx; 65 | } 66 | 67 | @Override 68 | public int getHeight() { 69 | return image.cy; 70 | } 71 | 72 | @Override 73 | public int size() { 74 | return image.cx * image.cy; 75 | } 76 | 77 | @Override 78 | public BufferedImage getImage() { 79 | return image.buf; 80 | } 81 | 82 | @Override 83 | public List getFeatureChannels() { 84 | return list; 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/info/hb/lookup/object/common/ImageBinaryScale.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object.common; 2 | 3 | import info.hb.lookup.object.Capture; 4 | import info.hb.lookup.object.Lookup; 5 | 6 | import java.awt.image.BufferedImage; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | public abstract class ImageBinaryScale { 11 | 12 | // original image 13 | public ImageBinary image; 14 | 15 | /** 16 | * for template we need several scales. 17 | * 18 | * due to gliches druing scale or big original images we may get disproportional scaled image. big or small objects 19 | * may be +1 pixed wider or toller. 20 | * 21 | * scales: normal scale, scale +1x, scale +1y, scale +1x+1y 22 | */ 23 | public List scales = new ArrayList<>(); 24 | 25 | // scale 0.5f for 50% 26 | public double s = 0; 27 | // blur kernel size 28 | public int k = 0; 29 | 30 | public void rescale(int s, int k) { 31 | rescale(project(s), k); 32 | } 33 | 34 | public double project(int s) { 35 | double m = Math.min(image.getWidth(), image.getHeight()); 36 | double q = m / s; 37 | 38 | q = Math.ceil(q); 39 | 40 | q = 1 / q; 41 | 42 | return q; 43 | } 44 | 45 | /** 46 | * create one resacle image 47 | * 48 | * @param s 49 | * @param k 50 | */ 51 | public void rescale(double s, int k) { 52 | scales.clear(); 53 | 54 | this.s = s; 55 | this.k = k; 56 | 57 | scales.add(rescale(Lookup.scale(image.getImage(), s, k))); 58 | } 59 | 60 | /** 61 | * create compensated images, 0,0 +1,0 0,+1 +1,+1 62 | * 63 | * @param s 64 | * @param k 65 | */ 66 | public void rescaleMosaic(double s, int k) { 67 | scales.clear(); 68 | 69 | this.s = s; 70 | this.k = k; 71 | 72 | scales.add(rescaleCrop(Lookup.scale(image.getImage(), s, k))); 73 | // scales.add(rescaleCrop(Lookup.scale(image.getImage(), s, k, +1, 0))); 74 | // scales.add(rescaleCrop(Lookup.scale(image.getImage(), s, k, 0, +1))); 75 | scales.add(rescaleCrop(Lookup.scale(image.getImage(), s, k, +1, +1))); 76 | 77 | } 78 | 79 | ImageBinary rescaleCrop(BufferedImage i) { 80 | if (i.getHeight() < 3 || i.getWidth() < 3) 81 | return rescale(i); 82 | else 83 | return rescale(Capture.crop(i, 1)); 84 | } 85 | 86 | abstract public ImageBinary rescale(BufferedImage i); 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/info/hb/lookup/object/common/ImageBinaryChannel.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object.common; 2 | 3 | /** 4 | * Minimal Sum-Tables required for any image for NCC or FNCC algorithm. 5 | * 6 | * 1) Base Image Array 7 | * 8 | * 2) Integral Image 9 | * 10 | * 3) Integral ^ 2 Image (Image Energy) 11 | * 12 | * 4) Zero Mean Image (image where each pixel substracted with image mean value) 13 | * 14 | */ 15 | public class ImageBinaryChannel { 16 | 17 | public enum ChannelType { 18 | GREY, RED, GREEN, BLUE 19 | } 20 | 21 | public SArray gi; 22 | public IntegralImage integral; 23 | public IntegralImage2 integral2; 24 | public ImageZeroMean zeroMean; 25 | public ChannelType type; 26 | 27 | public ImageBinaryChannel(ChannelType t) { 28 | this.type = t; 29 | integral = new IntegralImage(); 30 | integral2 = new IntegralImage2(); 31 | } 32 | 33 | public ImageBinaryChannel(ChannelType t, SArray img) { 34 | this.type = t; 35 | gi = img; 36 | integral = new IntegralImage(); 37 | integral2 = new IntegralImage2(); 38 | 39 | this.integral.initBase(gi); 40 | this.integral2.initBase(gi); 41 | 42 | for (int x = 0; x < this.gi.cx; x++) { 43 | for (int y = 0; y < this.gi.cy; y++) { 44 | step(x, y); 45 | } 46 | } 47 | 48 | zeroMean = new ImageZeroMean(integral); 49 | } 50 | 51 | public void step(int x, int y) { 52 | integral.step(x, y); 53 | integral2.step(x, y); 54 | } 55 | 56 | public void initBase(SArray img) { 57 | gi = img; 58 | integral.initBase(img); 59 | integral2.initBase(img); 60 | } 61 | 62 | public double dev2n() { 63 | return integral2.dev2n(integral); 64 | } 65 | 66 | public double dev2() { 67 | return integral2.dev2(integral); 68 | } 69 | 70 | public double dev() { 71 | return integral2.dev(integral); 72 | } 73 | 74 | public double dev2n(int x1, int y1, int x2, int y2) { 75 | return integral2.dev2n(integral, x1, y1, x2, y2); 76 | } 77 | 78 | public double dev2(int x1, int y1, int x2, int y2) { 79 | return integral2.dev2(integral, x1, y1, x2, y2); 80 | } 81 | 82 | public double dev(int x1, int y1, int x2, int y2) { 83 | return integral2.dev(integral, x1, y1, x2, y2); 84 | } 85 | 86 | public int getWidth() { 87 | return gi.cx; 88 | } 89 | 90 | public int getHeight() { 91 | return gi.cy; 92 | } 93 | 94 | public int size() { 95 | return gi.cx * gi.cy; 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/info/hb/lookup/object/common/SArray.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object.common; 2 | 3 | import info.hb.lookup.object.Capture; 4 | 5 | import java.awt.image.BufferedImage; 6 | 7 | /** 8 | * Double array wrapper class. Holds a double[] array and provides basic 9 | * methods. 10 | */ 11 | public class SArray { 12 | 13 | public SArray base; 14 | 15 | public int cx; 16 | public int cy; 17 | 18 | public double s[]; 19 | 20 | public SArray() { 21 | // 22 | } 23 | 24 | public SArray(int cx, int cy) { 25 | if (cx <= 0 || cy <= 0) 26 | throw new RuntimeException("wrong dimenssinons"); 27 | 28 | this.cx = cx; 29 | this.cy = cy; 30 | this.s = new double[cx * cy]; 31 | } 32 | 33 | public void initBase(SArray buf) { 34 | base = buf; 35 | 36 | cx = buf.cx; 37 | cy = buf.cy; 38 | 39 | s = new double[cx * cy]; 40 | } 41 | 42 | public double s(int x, int y) { 43 | if (x < 0) 44 | return 0; 45 | if (y < 0) 46 | return 0; 47 | return s[y * cx + x]; 48 | } 49 | 50 | public void s(int x, int y, double v) { 51 | if (x < 0) 52 | throw new RuntimeException("bad dim"); 53 | if (y < 0) 54 | throw new RuntimeException("bad dim"); 55 | s[y * cx + x] = v; 56 | } 57 | 58 | /** 59 | * Math Sigma (sum of the blocks) 60 | * 61 | * @param x1 62 | * @param y1 63 | * @param x2 64 | * @param y2 65 | * @return 66 | */ 67 | public double sigma(int x1, int y1, int x2, int y2) { 68 | double a = s(x1 - 1, y1 - 1); 69 | double b = s(x2, y1 - 1); 70 | double c = s(x1 - 1, y2); 71 | double d = s(x2, y2); 72 | return a + d - b - c; 73 | } 74 | 75 | public double sigma() { 76 | return sigma(0, 0, cx - 1, cy - 1); 77 | } 78 | 79 | public double mean(int x1, int y1, int x2, int y2) { 80 | double size = (x2 - x1 + 1) * (y2 - y1 + 1); 81 | return sigma(x1, y1, x2, y2) / size; 82 | } 83 | 84 | public double mean() { 85 | return mean(0, 0, cx - 1, cy - 1); 86 | } 87 | 88 | public void printDebug() { 89 | for (int y = 0; y < cy; y++) { 90 | for (int x = 0; x < cx; x++) { 91 | System.out.print(s(x, y)); 92 | System.out.print("\t"); 93 | } 94 | System.out.println(""); 95 | } 96 | } 97 | 98 | public BufferedImage getImage() { 99 | int[] ss = new int[s.length]; 100 | for (int i = 0; i < ss.length; i++) { 101 | int g = (int) s[i]; 102 | 103 | int argb = 0xff000000 | (g << 16) | (g << 8) | (g); 104 | 105 | ss[i] = argb; 106 | } 107 | 108 | BufferedImage image = new BufferedImage(cx, cy, BufferedImage.TYPE_INT_ARGB); 109 | image.getWritableTile(0, 0).setDataElements(0, 0, image.getWidth(), image.getHeight(), ss); 110 | return image; 111 | } 112 | 113 | public void writeDesktop() { 114 | Capture.writeDesktop(getImage()); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /src/main/java/info/hb/lookup/object/common/ImageBinaryRGBFeature.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object.common; 2 | 3 | import info.hb.lookup.object.common.ImageBinaryChannel.ChannelType; 4 | 5 | import java.awt.image.BufferedImage; 6 | import java.util.Arrays; 7 | import java.util.List; 8 | 9 | /** 10 | * Container for ImageBinary and Feature class for each channel (rgb channels here) 11 | */ 12 | public class ImageBinaryRGBFeature implements ImageBinaryFeature { 13 | 14 | public RGBImage image; 15 | public ImageBinaryChannelFeature r; 16 | public ImageBinaryChannelFeature g; 17 | public ImageBinaryChannelFeature b; 18 | 19 | public List list; 20 | 21 | public ImageBinaryRGBFeature(BufferedImage img, double threshold) { 22 | init(img); 23 | 24 | FeatureSet lr = new FeatureSetAuto(r, threshold); 25 | r.init(lr); 26 | 27 | FeatureSet lg = new FeatureSetAuto(g, threshold); 28 | g.init(lg); 29 | 30 | FeatureSet lb = new FeatureSetAuto(b, threshold); 31 | b.init(lb); 32 | } 33 | 34 | public void init(BufferedImage img) { 35 | image = new RGBImage(); 36 | r = new ImageBinaryChannelFeature(ChannelType.RED); 37 | g = new ImageBinaryChannelFeature(ChannelType.GREEN); 38 | b = new ImageBinaryChannelFeature(ChannelType.BLUE); 39 | 40 | list = Arrays.asList(new ImageBinaryChannelFeature[] { r, g, b }); 41 | 42 | this.image.init(img); 43 | this.r.initBase(this.image.r); 44 | this.g.initBase(this.image.g); 45 | this.b.initBase(this.image.b); 46 | 47 | for (int x = 0; x < this.image.cx; x++) { 48 | for (int y = 0; y < this.image.cy; y++) { 49 | this.image.step(x, y); 50 | this.r.step(x, y); 51 | this.g.step(x, y); 52 | this.b.step(x, y); 53 | } 54 | } 55 | 56 | r.zeroMean = new ImageZeroMean(); 57 | g.zeroMean = new ImageZeroMean(); 58 | b.zeroMean = new ImageZeroMean(); 59 | r.zeroMean.init(r.integral); 60 | g.zeroMean.init(g.integral); 61 | b.zeroMean.init(b.integral); 62 | 63 | for (int x = 0; x < this.image.cx; x++) { 64 | for (int y = 0; y < this.image.cy; y++) { 65 | r.zeroMean.step(x, y); 66 | g.zeroMean.step(x, y); 67 | b.zeroMean.step(x, y); 68 | } 69 | } 70 | 71 | r.zeroMeanIntegral = new IntegralImage(); 72 | g.zeroMeanIntegral = new IntegralImage(); 73 | b.zeroMeanIntegral = new IntegralImage(); 74 | r.zeroMeanIntegral.initBase(r.zeroMean); 75 | g.zeroMeanIntegral.initBase(g.zeroMean); 76 | b.zeroMeanIntegral.initBase(b.zeroMean); 77 | 78 | for (int x = 0; x < this.image.cx; x++) { 79 | for (int y = 0; y < this.image.cy; y++) { 80 | r.zeroMeanIntegral.step(x, y); 81 | g.zeroMeanIntegral.step(x, y); 82 | b.zeroMeanIntegral.step(x, y); 83 | } 84 | } 85 | } 86 | 87 | @Override 88 | public int getWidth() { 89 | return image.cx; 90 | } 91 | 92 | @Override 93 | public int getHeight() { 94 | return image.cy; 95 | } 96 | 97 | @Override 98 | public int size() { 99 | return image.cx * image.cy; 100 | } 101 | 102 | @Override 103 | public BufferedImage getImage() { 104 | return image.buf; 105 | } 106 | 107 | @Override 108 | public List getFeatureChannels() { 109 | return list; 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/info/hb/lookup/object/common/RectK.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object.common; 2 | 3 | /** 4 | * Haar like Feature rect with it's value k. 5 | */ 6 | public class RectK implements Comparable { 7 | 8 | public int x1; 9 | public int y1; 10 | public int x2; 11 | public int y2; 12 | 13 | // base rect cx size 14 | public int cxBase; 15 | // base rect cy size 16 | public int cyBase; 17 | 18 | // sum of the pixels in the area 19 | public double k; 20 | 21 | public RectK(int x, int y) { 22 | x1 = x; 23 | y1 = y; 24 | x2 = x; 25 | y2 = y; 26 | } 27 | 28 | public RectK(int x1, int y1, int x2, int y2, int cx, int cy) { 29 | this.x1 = x1; 30 | this.y1 = y1; 31 | this.x2 = x2; 32 | this.y2 = y2; 33 | 34 | this.cxBase = cx; 35 | this.cyBase = cy; 36 | } 37 | 38 | public int getWidth() { 39 | return x2 - x1 + 1; 40 | } 41 | 42 | public int getHeight() { 43 | return y2 - y1 + 1; 44 | } 45 | 46 | public int size() { 47 | return (x2 - x1 + 1) * (y2 - y1 + 1); 48 | } 49 | 50 | public boolean equal(RectK k) { 51 | return x1 == k.x1 && x2 == k.x2 && y1 == k.y1 && y2 == k.y2; 52 | } 53 | 54 | @Override 55 | public int compareTo(RectK k) { 56 | int r = 0; 57 | 58 | if (r == 0) 59 | r = new Integer(x1).compareTo(k.x1); 60 | 61 | if (r == 0) 62 | r = new Integer(y1).compareTo(k.y1); 63 | 64 | if (r == 0) 65 | r = new Integer(x2).compareTo(k.x2); 66 | 67 | if (r == 0) 68 | r = new Integer(y2).compareTo(k.y2); 69 | 70 | return r; 71 | } 72 | 73 | public RectK[] devide() { 74 | 75 | int w = getWidth(); 76 | int h = getHeight(); 77 | if (w > h) { 78 | w = w / 2; 79 | 80 | RectK r1 = new RectK(x1, y1, x1 + w - 1, y1 + h - 1, cxBase, cyBase); 81 | 82 | int r2x1 = r1.x2 + 1; 83 | int r2y1 = r1.y1; 84 | int r2cx = getWidth() - r1.getWidth(); 85 | int r2cy = getHeight(); 86 | 87 | RectK r2 = new RectK(r2x1, r2y1, r2x1 + r2cx - 1, r2y1 + r2cy - 1, cxBase, cyBase); 88 | 89 | if (r1.getWidth() <= 0 || r1.getHeight() <= 0) 90 | throw null; 91 | if (r2.getWidth() <= 0 || r2.getHeight() <= 0) 92 | throw null; 93 | 94 | return new RectK[] { r1, r2 }; 95 | } else { 96 | h = h / 2; 97 | 98 | RectK r1 = new RectK(x1, y1, x1 + w - 1, y1 + h - 1, cxBase, cyBase); 99 | 100 | int r2x1 = r1.x1; 101 | int r2y1 = r1.y2 + 1; 102 | int r2cx = getWidth(); 103 | int r2cy = getHeight() - r1.getHeight(); 104 | 105 | RectK r2 = new RectK(r2x1, r2y1, r2x1 + r2cx - 1, r2y1 + r2cy - 1, cxBase, cyBase); 106 | 107 | if (r1.getWidth() <= 0 || r1.getHeight() <= 0) 108 | return null; 109 | if (r2.getWidth() <= 0 || r2.getHeight() <= 0) 110 | throw null; 111 | 112 | return new RectK[] { r1, r2 }; 113 | } 114 | } 115 | 116 | public Feature getFeature() { 117 | 118 | SArray s = new SArray(cxBase, cyBase); 119 | 120 | int c = 0; 121 | 122 | for (int x = 0; x < s.cx; x++) { 123 | for (int y = 0; y < s.cy; y++) { 124 | boolean test = x >= x1 && x <= x2 && y >= y1 && y <= y2; 125 | int v = test ? 1 : 0; 126 | s.s(x, y, v); 127 | 128 | c += v; 129 | } 130 | } 131 | 132 | // for debug purpose 133 | if (c == 0) 134 | throw new RuntimeException("empty feature"); 135 | 136 | return new Feature(s); 137 | } 138 | 139 | } 140 | -------------------------------------------------------------------------------- /src/main/java/info/hb/lookup/object/OCRScale.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object; 2 | 3 | import info.hb.lookup.object.common.FontSymbol; 4 | import info.hb.lookup.object.common.FontSymbolLookup; 5 | import info.hb.lookup.object.common.ImageBinary; 6 | import info.hb.lookup.object.common.ImageBinaryGreyScale; 7 | import info.hb.lookup.object.common.ImageBinaryScale; 8 | 9 | import java.awt.image.BufferedImage; 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | /** 14 | * For big images you may want scale booth image and template image similarly. (lets say booth reduce by 2x, you will 15 | * still have same recognitionz quality but it goes twice faster on recognition step) 16 | */ 17 | public class OCRScale extends OCR { 18 | 19 | public double s = 0; 20 | public int defaultBlurKernel; 21 | 22 | /** 23 | * @param scaleSize 24 | * ex:5 25 | * 26 | * @param blurKernel 27 | * ex:10 28 | */ 29 | public OCRScale(float s, int blurKernel, float threshold) { 30 | super(threshold); 31 | this.s = s; 32 | this.defaultBlurKernel = blurKernel; 33 | } 34 | 35 | @Override 36 | public String recognize(BufferedImage bi) { 37 | ImageBinaryScale i = new ImageBinaryGreyScale(bi); 38 | 39 | return recognize(i); 40 | } 41 | 42 | public String recognize(ImageBinaryScale i) { 43 | List list = getSymbols(); 44 | 45 | return recognize(i, 0, 0, i.image.getWidth() - 1, i.image.getHeight() - 1, list); 46 | } 47 | 48 | @Override 49 | public String recognize(BufferedImage bi, String fontSet) { 50 | ImageBinaryScale i = new ImageBinaryGreyScale(bi); 51 | 52 | return recognize(i, fontSet); 53 | } 54 | 55 | public String recognize(ImageBinaryScale i, String fontSet) { 56 | List list = getSymbols(fontSet); 57 | 58 | return recognize(i, 0, 0, i.image.getWidth() - 1, i.image.getHeight() - 1, list); 59 | } 60 | 61 | public String recognize(ImageBinaryScale i, int x1, int y1, int x2, int y2) { 62 | List list = getSymbols(); 63 | 64 | return recognize(i, x1, y1, x2, y2, list); 65 | } 66 | 67 | public String recognize(ImageBinaryScale i, int x1, int y1, int x2, int y2, String fontFamily) { 68 | List list = getSymbols(fontFamily); 69 | 70 | return recognize(i, x1, y1, x2, y2, list); 71 | } 72 | 73 | public String recognize(ImageBinaryScale i, int x1, int y1, int x2, int y2, List list) { 74 | for (FontSymbol s : list) { 75 | scale(i, s.image); 76 | } 77 | 78 | // before this point we operating on original image pixels. after it, we are operating on scaled coords 79 | 80 | x1 *= s; 81 | y1 *= s; 82 | x2 *= s; 83 | y2 *= s; 84 | 85 | List all = new ArrayList<>(); 86 | 87 | for (ImageBinary iScaleBin : i.scales) { 88 | // rounding can be 1 pixels off images end 89 | if (x2 >= iScaleBin.getWidth()) 90 | x2 = iScaleBin.getWidth() - 1; 91 | if (y2 >= iScaleBin.getHeight()) 92 | y2 = iScaleBin.getHeight() - 1; 93 | 94 | all.addAll(findAll(list, iScaleBin, x1, y1, x2, y2)); 95 | } 96 | 97 | return recognize(all); 98 | } 99 | 100 | public void scale(ImageBinaryScale image, ImageBinaryScale template) { 101 | if (s == 0) { 102 | s = template.s; 103 | } 104 | 105 | if (s != template.s) { 106 | template.rescaleMosaic(s, defaultBlurKernel); 107 | } 108 | 109 | if (s != image.s) { 110 | image.rescale(s, defaultBlurKernel); 111 | } 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /src/main/java/info/hb/lookup/object/OCRCore.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object; 2 | 3 | import info.hb.lookup.object.common.FontFamily; 4 | import info.hb.lookup.object.common.FontSymbol; 5 | import info.hb.lookup.object.common.FontSymbolLookup; 6 | import info.hb.lookup.object.common.GPoint; 7 | import info.hb.lookup.object.common.ImageBinary; 8 | import info.hb.lookup.object.common.ImageBinaryGrey; 9 | import info.hb.lookup.object.common.LessCompare; 10 | import info.hb.lookup.object.proc.CannyEdgeDetector; 11 | import info.hb.lookup.object.proc.NCC; 12 | 13 | import java.util.ArrayList; 14 | import java.util.Comparator; 15 | import java.util.HashMap; 16 | import java.util.List; 17 | import java.util.Map; 18 | 19 | public class OCRCore { 20 | 21 | static public class BiggerFirst implements Comparator { 22 | 23 | public int maxSize; 24 | public int maxSize2; 25 | 26 | public BiggerFirst(List list) { 27 | maxSize = 0; 28 | 29 | for (FontSymbolLookup l : list) { 30 | maxSize = Math.max(maxSize, l.size()); 31 | } 32 | 33 | maxSize2 = maxSize / 2; 34 | } 35 | 36 | @Override 37 | public int compare(FontSymbolLookup arg0, FontSymbolLookup arg1) { 38 | int r = LessCompare.compareBigFirst(arg0.size(), arg1.size(), maxSize2); 39 | 40 | // better quality goes first 41 | if (r == 0) 42 | r = LessCompare.compareBigFirst(arg0.g, arg1.g); 43 | 44 | // bigger items goes first 45 | if (r == 0) 46 | r = LessCompare.compareBigFirst(arg0.size(), arg1.size()); 47 | 48 | return r; 49 | } 50 | 51 | } 52 | 53 | static public class Left2Right implements Comparator { 54 | 55 | @Override 56 | public int compare(FontSymbolLookup arg0, FontSymbolLookup arg1) { 57 | int r = 0; 58 | 59 | if (r == 0) { 60 | if (!arg0.yCross(arg1)) 61 | r = LessCompare.compareSmallFirst(arg0.y, arg1.y); 62 | } 63 | 64 | if (r == 0) 65 | r = LessCompare.compareSmallFirst(arg0.x, arg1.x); 66 | 67 | if (r == 0) 68 | r = LessCompare.compareSmallFirst(arg0.y, arg1.y); 69 | 70 | return r; 71 | } 72 | 73 | } 74 | 75 | public Map fontFamily = new HashMap<>(); 76 | 77 | public CannyEdgeDetector detector = new CannyEdgeDetector(); 78 | 79 | // 1.0f == exact match, -1.0f - completely different images 80 | public float threshold = 0.70f; 81 | 82 | public OCRCore(float threshold) { 83 | this.threshold = threshold; 84 | 85 | detector.setLowThreshold(3f); 86 | detector.setHighThreshold(3f); 87 | detector.setGaussianKernelWidth(2); 88 | detector.setGaussianKernelRadius(1f); 89 | } 90 | 91 | public List getSymbols() { 92 | List list = new ArrayList<>(); 93 | 94 | for (FontFamily f : fontFamily.values()) { 95 | list.addAll(f); 96 | } 97 | 98 | return list; 99 | } 100 | 101 | public List getSymbols(String fontFamily) { 102 | return this.fontFamily.get(fontFamily); 103 | } 104 | 105 | public List findAll(List list, ImageBinaryGrey bi) { 106 | return findAll(list, bi, 0, 0, bi.getWidth(), bi.getHeight()); 107 | } 108 | 109 | public List findAll(List list, ImageBinary bi, int x1, int y1, int x2, int y2) { 110 | List l = new ArrayList<>(); 111 | 112 | for (FontSymbol fs : list) { 113 | for (ImageBinary imageScaleBin : fs.image.scales) { 114 | List ll = NCC.lookupAll(bi, x1, y1, x2, y2, imageScaleBin, threshold); 115 | for (GPoint p : ll) 116 | l.add(new FontSymbolLookup(fs, p.x, p.y, p.g)); 117 | } 118 | } 119 | 120 | return l; 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/info/hb/lookup/object/proc/FNCC.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object.proc; 2 | 3 | import info.hb.lookup.object.Lookup.NotFound; 4 | import info.hb.lookup.object.common.FeatureK; 5 | import info.hb.lookup.object.common.GFirst; 6 | import info.hb.lookup.object.common.GPoint; 7 | import info.hb.lookup.object.common.ImageBinary; 8 | import info.hb.lookup.object.common.ImageBinaryChannel; 9 | import info.hb.lookup.object.common.ImageBinaryChannelFeature; 10 | import info.hb.lookup.object.common.ImageBinaryFeature; 11 | import info.hb.lookup.object.common.ImageBinaryRGB; 12 | import info.hb.lookup.object.common.ImageBinaryRGBFeature; 13 | import info.hb.lookup.object.common.RectK; 14 | import info.hb.lookup.object.proc.NCC.WrongChannelType; 15 | 16 | import java.awt.image.BufferedImage; 17 | import java.util.ArrayList; 18 | import java.util.Collections; 19 | import java.util.List; 20 | 21 | /** 22 | * http://isas.uka.de/Material/AltePublikationen/briechle_spie2001.pdf 23 | * 24 | * NOT WORKING (check NCC.java) 25 | * 26 | * Fast Normalized cross correlation algorithm 27 | */ 28 | public class FNCC { 29 | 30 | static public List lookupAll(BufferedImage i, BufferedImage t, double threshold, float m) { 31 | ImageBinaryRGB imageBinary = new ImageBinaryRGB(i); 32 | ImageBinaryRGBFeature templateBinary = new ImageBinaryRGBFeature(t, threshold); 33 | 34 | return lookupAll(imageBinary, templateBinary, m); 35 | } 36 | 37 | static public GPoint lookup(ImageBinary image, ImageBinaryFeature template, float m) { 38 | List list = lookupAll(image, template, m); 39 | 40 | if (list.size() == 0) 41 | throw new NotFound(); 42 | 43 | Collections.sort(list, new GFirst()); 44 | 45 | return list.get(0); 46 | } 47 | 48 | static public List lookupAll(ImageBinary image, ImageBinaryFeature template, float m) { 49 | return lookupAll(image, 0, 0, image.getWidth() - 1, image.getHeight() - 1, template, m); 50 | } 51 | 52 | static public List lookupAll(ImageBinary image, int x1, int y1, int x2, int y2, 53 | ImageBinaryFeature template, float m) { 54 | List list = new ArrayList<>(); 55 | 56 | for (int x = x1; x <= x2 - template.getWidth() + 1; x++) { 57 | for (int y = y1; y <= y2 - template.getHeight() + 1; y++) { 58 | List ci = image.getChannels(); 59 | List ct = template.getFeatureChannels(); 60 | 61 | int ii = Math.min(ci.size(), ct.size()); 62 | 63 | boolean b = true; 64 | double gg = Double.MAX_VALUE; 65 | 66 | for (int i = 0; i < ii; i++) { 67 | ImageBinaryChannelFeature cct = ct.get(i); 68 | ImageBinaryChannel cci = ci.get(i); 69 | 70 | if (!cct.type.equals(cci.type)) 71 | throw new WrongChannelType(); 72 | 73 | double g = gamma(cci, cct, x, y); 74 | 75 | gg = Math.min(gg, g); 76 | 77 | if (g < m) { 78 | b = false; 79 | } 80 | } 81 | 82 | if (b) { 83 | list.add(new GPoint(x, y, gg)); 84 | } 85 | } 86 | } 87 | 88 | return list; 89 | } 90 | 91 | static double denominator(ImageBinaryChannel image, ImageBinaryChannel template, int xx, int yy) { 92 | double di = image.dev2n(xx, yy, xx + template.getWidth() - 1, yy + template.getHeight() - 1); 93 | double dt = template.dev2n(); 94 | return Math.sqrt(di * dt); 95 | } 96 | 97 | static double numerator(ImageBinaryChannel image, ImageBinaryChannelFeature template, int xx, int yy) { 98 | double n = 0; 99 | 100 | for (FeatureK f : template.k) { 101 | for (RectK k : f.list) { 102 | double ii = image.integral.sigma(xx + k.x1, yy + k.y1, xx + k.x2, yy + k.y2); 103 | double mt = k.k; 104 | n += ii * mt; 105 | } 106 | } 107 | 108 | return n; 109 | } 110 | 111 | static public double gamma(ImageBinaryChannel image, ImageBinaryChannelFeature template, int xx, int yy) { 112 | double d = denominator(image, template, xx, yy); 113 | 114 | if (d == 0) 115 | return -1; 116 | 117 | double n = numerator(image, template, xx, yy); 118 | 119 | return (n / d); 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | info.hb 6 | image-text-extract 7 | 0.0.1-SNAPSHOT 8 | Image Text Extract 9 | 10 | 11 | UTF-8 12 | 1.0 13 | 14 | 15 | 16 | 17 | 18 | de.vorb 19 | jtesseract 20 | 0.0.4 21 | 22 | 23 | org.bytedeco.javacpp-presets 24 | tesseract 25 | 3.03-rc1-1.0 26 | 27 | 28 | net.sourceforge.tess4j 29 | tess4j 30 | 2.0.0 31 | 32 | 33 | 34 | net.sourceforge.javaocr.plugins 35 | javaocr-plugin-moment 36 | ${ocr.version} 37 | 38 | 39 | net.sourceforge.javaocr.plugins 40 | javaocr-plugin-cluster 41 | ${ocr.version} 42 | 43 | 44 | net.sourceforge.javaocr.plugins 45 | javaocr-plugin-fir 46 | ${ocr.version} 47 | 48 | 49 | net.sourceforge.javaocr.plugins 50 | javaocr-plugin-morphology 51 | ${ocr.version} 52 | 53 | 54 | net.sourceforge.javaocr.plugins 55 | javaocr-plugin-awt 56 | ${ocr.version} 57 | 58 | 59 | net.sourceforge.javaocr 60 | javaocr-core 61 | ${ocr.version} 62 | 63 | 64 | 65 | commons-cli 66 | commons-cli 67 | 1.2 68 | 69 | 70 | com.github.axet 71 | desktop 72 | 2.2.3 73 | 74 | 75 | commons-io 76 | commons-io 77 | 2.4 78 | 79 | 80 | commons-lang 81 | commons-lang 82 | 2.6 83 | 84 | 85 | com.jhlabs 86 | filters 87 | 2.0.235-1 88 | 89 | 90 | 91 | junit 92 | junit 93 | 4.11 94 | test 95 | 96 | 97 | 98 | 99 | 100 | 101 | org.apache.maven.plugins 102 | maven-surefire-plugin 103 | 2.12.4 104 | 105 | 106 | **/TestUtils.java 107 | **/TestUtils$*.java 108 | 109 | **/TestUtils.*.java 110 | 111 | 112 | 113 | 114 | org.apache.maven.plugins 115 | maven-release-plugin 116 | 2.3.2 117 | 118 | false 119 | true 120 | install 121 | true 122 | deploy 123 | 124 | 125 | 126 | org.apache.maven.plugins 127 | maven-compiler-plugin 128 | 2.5.1 129 | 130 | 1.7 131 | 1.7 132 | UTF-8 133 | 134 | 135 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /src/main/java/info/hb/lookup/object/common/RangeColor.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object.common; 2 | 3 | /** 4 | * Range Color object. Can hold range color value. (minimum and maximum of specified color) in the int-rgb value 5 | */ 6 | public class RangeColor { 7 | 8 | public int min; 9 | public int max; 10 | 11 | public RangeColor(RangeColor range) { 12 | this.min = range.min; 13 | this.max = range.max; 14 | } 15 | 16 | /** 17 | * create range from mid 18 | * 19 | * @param mid 20 | * @param diff 21 | */ 22 | public RangeColor(int rgb, float f) { 23 | rgb &= 0x00ffffff; 24 | 25 | int diff = (int) (255 * f); 26 | 27 | int r = (rgb & 0xff0000) >> 16; 28 | int g = (rgb & 0x00ff00) >> 8; 29 | int b = (rgb & 0x0000ff) >> 0; 30 | 31 | int rm = r - diff; 32 | if (rm < 0) 33 | rm = 0; 34 | int gm = g - diff; 35 | if (gm < 0) 36 | gm = 0; 37 | int bm = b - diff; 38 | if (bm < 0) 39 | bm = 0; 40 | 41 | this.min = rm << 16 | gm << 8 | bm; 42 | 43 | int rb = r + diff; 44 | if (rb > 255) 45 | rb = 255; 46 | int gb = g + diff; 47 | if (gb > 255) 48 | gb = 255; 49 | int bb = b + diff; 50 | if (bb > 255) 51 | bb = 255; 52 | 53 | this.max = rb << 16 | gb << 8 | bb; 54 | } 55 | 56 | public RangeColor(int min, int max) { 57 | this.min = min; 58 | this.max = max; 59 | } 60 | 61 | /** 62 | * if rgb1 > rgb2 return true 63 | * 64 | * @param rgb1 65 | * @param rgb2 66 | * @return 67 | */ 68 | public static boolean isGr(int rgb1, int rgb2) { 69 | int r1 = rgb1 & 0xff0000; 70 | int g1 = rgb1 & 0x00ff00; 71 | int b1 = rgb1 & 0x0000ff; 72 | 73 | int r2 = rgb2 & 0xff0000; 74 | int g2 = rgb2 & 0x00ff00; 75 | int b2 = rgb2 & 0x0000ff; 76 | 77 | return (r1 > r2) || (g1 > g2) || (b1 > b2); 78 | } 79 | 80 | public boolean inRange(RangeColor r) { 81 | return inRange(r.min) || inRange(r.max); 82 | } 83 | 84 | public boolean inRange(int rgb) { 85 | int r = rgb & 0xff0000; 86 | int g = rgb & 0x00ff00; 87 | int b = rgb & 0x0000ff; 88 | 89 | int rl = min & 0xff0000; 90 | int gl = min & 0x00ff00; 91 | int bl = min & 0x0000ff; 92 | 93 | int rh = max & 0xff0000; 94 | int gh = max & 0x00ff00; 95 | int bh = max & 0x0000ff; 96 | 97 | return (r >= rl && r <= rh) && (g >= gl && g <= gh) && (b >= bl && b <= bh); 98 | } 99 | 100 | public int getDistance(int rgb) { 101 | int r1 = (rgb & 0xff0000) >> 16; 102 | int g1 = (rgb & 0x00ff00) >> 8; 103 | int b1 = (rgb & 0x0000ff) >> 0; 104 | 105 | int rl = (min & 0xff0000) >> 16; 106 | int gl = (min & 0x00ff00) >> 8; 107 | int bl = (min & 0x0000ff) >> 0; 108 | 109 | int rh = (max & 0xff0000) >> 16; 110 | int gh = (max & 0x00ff00) >> 8; 111 | int bh = (max & 0x0000ff) >> 0; 112 | 113 | if ((r1 >= rl && r1 <= rh) && (g1 >= gl && g1 <= gh) && (b1 >= bl && b1 <= bh)) 114 | return 0; 115 | 116 | int total = 0; 117 | 118 | if (r1 < rl) 119 | total = rl - r1; 120 | 121 | if (r1 > rh) 122 | total += r1 - rh; 123 | 124 | if (g1 < gl) 125 | total += gl - g1; 126 | 127 | if (g1 > gh) 128 | total += g1 - gh; 129 | 130 | if (b1 < bl) 131 | total += bl - b1; 132 | 133 | if (b1 > bh) 134 | total += b1 - bh; 135 | 136 | return total; 137 | } 138 | 139 | public void merge(RangeColor color) { 140 | merge(color.min); 141 | merge(color.max); 142 | } 143 | 144 | public void merge(int rgb) { 145 | int r1 = (rgb & 0xff0000) >> 16; 146 | int g1 = (rgb & 0x00ff00) >> 8; 147 | int b1 = (rgb & 0x0000ff) >> 0; 148 | 149 | int rl = (min & 0xff0000) >> 16; 150 | int gl = (min & 0x00ff00) >> 8; 151 | int bl = (min & 0x0000ff) >> 0; 152 | 153 | int rh = (max & 0xff0000) >> 16; 154 | int gh = (max & 0x00ff00) >> 8; 155 | int bh = (max & 0x0000ff) >> 0; 156 | 157 | rl = Math.min(rl, r1); 158 | gl = Math.min(gl, g1); 159 | bl = Math.min(bl, b1); 160 | 161 | rh = Math.max(rh, r1); 162 | gh = Math.max(gh, g1); 163 | bh = Math.max(bh, b1); 164 | 165 | min = (rl << 16) | (gl << 8) | (bl); 166 | max = (rh << 16) | (gh << 8) | (bh); 167 | } 168 | 169 | public int av(int l, int h) { 170 | return l + (h - l) / 2; 171 | } 172 | 173 | public int average() { 174 | int rl = (min & 0xff0000) >> 16; 175 | int gl = (min & 0x00ff00) >> 8; 176 | int bl = (min & 0x0000ff) >> 0; 177 | 178 | int rh = (max & 0xff0000) >> 16; 179 | int gh = (max & 0x00ff00) >> 8; 180 | int bh = (max & 0x0000ff) >> 0; 181 | 182 | return (av(rl, rh) << 16) | (av(gl, gh) << 8) | (av(bl, bh)); 183 | } 184 | 185 | public void extend(int a, float f) { 186 | RangeColor cc = new RangeColor(a, f); 187 | merge(cc.min); 188 | merge(cc.max); 189 | } 190 | 191 | } 192 | -------------------------------------------------------------------------------- /src/main/java/info/hb/lookup/object/common/ClassResources.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object.common; 2 | 3 | import java.io.File; 4 | import java.security.CodeSource; 5 | import java.util.ArrayList; 6 | import java.util.Arrays; 7 | import java.util.Enumeration; 8 | import java.util.HashSet; 9 | import java.util.List; 10 | import java.util.Set; 11 | import java.util.jar.JarEntry; 12 | import java.util.jar.JarFile; 13 | 14 | import org.apache.commons.lang.StringUtils; 15 | 16 | /** 17 | * it worth to be a separate project 18 | * 19 | * c = ClassResources(YourClass.class, "fonts") c.names(); 20 | * 21 | * fonts is a resources directory: 22 | * 23 | * com/example/project/YourClass.class 24 | * 25 | * com/example/project/fonts 26 | * 27 | * TODO make it separate project 28 | */ 29 | public class ClassResources { 30 | 31 | public Class c; 32 | public File path; 33 | 34 | public ClassResources(Class c) { 35 | this.c = c; 36 | } 37 | 38 | public ClassResources(Class c, File path) { 39 | this.c = c; 40 | this.path = path; 41 | } 42 | 43 | /** 44 | * 45 | * @param path 46 | * @return 47 | */ 48 | public List names() { 49 | return getResourceListing(c, path); 50 | } 51 | 52 | /** 53 | * enter sub directory 54 | * 55 | * @param path 56 | * @return 57 | */ 58 | public ClassResources dir(File path) { 59 | return new ClassResources(c, new File(this.path, path.getPath())); 60 | } 61 | 62 | // 1) under debugger, /Users/axet/source/mircle/play/target/classes/ 63 | // 64 | // 2) app packed as one jar, mac osx wihtout debugger path - 65 | // /Users/axet/source/mircle/mircle/macosx/Mircle.app/Contents/Resources/Java/mircle.jar 66 | // case above 1) works prefectly 67 | // 68 | // 3) if it is a separate library packed with maven under debugger 69 | // /Users/axet/.m2/repository/com/github/axet/play/0.0.3/play-0.0.3.jar 70 | File getPath(Class cls) { 71 | CodeSource src = cls.getProtectionDomain().getCodeSource(); 72 | 73 | if (src == null) 74 | return null; 75 | 76 | return new File(src.getLocation().getPath()); 77 | } 78 | 79 | String getClassPath(Class c) { 80 | return new File(c.getCanonicalName().replace('.', File.separatorChar)).getParent(); 81 | } 82 | 83 | String getClassPath(Class c, String path) { 84 | return new File(c.getCanonicalName().replace('.', File.separatorChar)).getParent() + File.separator + path; 85 | } 86 | 87 | List getResourceListing(Class clazz, File path) { 88 | try { 89 | // clazz.getClassLoader().getResource(pp) may return system library 90 | // if path is common (Like "/com") 91 | File pp = getPath(clazz); 92 | 93 | String strPath = path.getPath(); 94 | 95 | if (pp.isDirectory()) { 96 | if (strPath.startsWith(File.separator)) 97 | pp = new File(pp, strPath); 98 | else 99 | pp = new File(pp, getClassPath(clazz, strPath)); 100 | 101 | File r = new File(pp.toURI()); 102 | 103 | if (!r.exists()) 104 | throw new RuntimeException("File not found: " + r); 105 | 106 | String[] ss = r.list(); 107 | if (ss == null) 108 | return new ArrayList(); 109 | 110 | if (ss.length == 0) 111 | throw new RuntimeException("Font directory is empy: " + r); 112 | 113 | return Arrays.asList(ss); 114 | } 115 | 116 | if (pp.isFile()) { 117 | String p; 118 | 119 | if (strPath.startsWith(File.separator)) 120 | p = StringUtils.removeStart(strPath, File.separator); 121 | else 122 | p = getClassPath(clazz, strPath); 123 | 124 | JarFile jar = new JarFile(pp); 125 | Enumeration entries = jar.entries(); 126 | jar.close(); 127 | Set result = new HashSet<>(); 128 | 129 | boolean f = false; 130 | while (entries.hasMoreElements()) { 131 | String name = entries.nextElement().getName(); 132 | if (name.startsWith(p)) { 133 | f = true; 134 | String a = StringUtils.removeStart(name, p); 135 | a = StringUtils.removeStart(a, File.separator); 136 | a = a.trim(); 137 | 138 | int e = StringUtils.indexOfAny(a, new char[] { '/', '\\' }); 139 | if (e != -1) 140 | a = a.substring(0, e); 141 | 142 | if (!a.isEmpty()) 143 | result.add(a); 144 | } 145 | } 146 | 147 | if (!f) 148 | throw new RuntimeException("Jar file: " + pp + " has no entry: " + p); 149 | 150 | if (result.isEmpty()) 151 | throw new RuntimeException("Jar file: " + pp + " has empty folder: " + p); 152 | 153 | return new ArrayList(result); 154 | } 155 | } catch (RuntimeException e) { 156 | throw e; 157 | } catch (Exception e) { 158 | throw new RuntimeException(e); 159 | } 160 | 161 | return null; 162 | } 163 | 164 | } 165 | -------------------------------------------------------------------------------- /src/main/java/info/hb/lookup/object/README.md: -------------------------------------------------------------------------------- 1 | 2 | ## 从图片重提取目标 3 | 4 | It is a nice, simple and friendly to use library which helps you to lookup objects on a screen. Also it has a OCR functionality. 5 | 6 | Using Lookup you can do Java OCR tricks like recognizing any infromation from your Robot application. Which can be 7 | usefull for debuging or automating things. 8 | 9 | Lookup project designed to lookup images in images. It based on NCC algorithm. Turns out it is good for simple OCR purpose as well. 10 | 11 | Details on NCC (Normalized cross correlation) can be found in 'doc' folder (a lot of math). 12 | 13 | Choose 'lookup' over OpenCL if you need fast simple, pure java solution. 14 | 15 | ## OCR功能 16 | 17 | If you need to encode special symbols use UNICODE in the file name. For example if you need to have '\' character (which is prohibited 18 | in the path and file name) specify %2F.png as a image symbol name. 19 | 20 | Sometimes you need specify two different image for one symbol (if image / font symbol varry too mutch). To do so add unicode ZERO WIDTH SPACE symbol to the filename. Like that %2F%E2%80%8B.png will produce '/' symbol as well. 21 | 22 | package com.github.axet.lookup; 23 | 24 | import java.io.File; 25 | 26 | import com.github.axet.lookup.common.ImageBinaryGrey; 27 | 28 | public class OCRTest { 29 | 30 | static public void main(String[] args) { 31 | OCR l = new OCR(0.70f); 32 | 33 | // will go to com/github/axet/lookup/fonts folder and load all font 34 | // familys (here is only font_1 family in this library) 35 | l.loadFontsDirectory(OCRTest.class, new File("fonts")); 36 | 37 | // example how to load only one family 38 | // "com/github/axet/lookup/fonts/font_1" 39 | l.loadFont(OCRTest.class, new File("fonts", "font_1")); 40 | 41 | String str = ""; 42 | 43 | // recognize using all familys set 44 | str = l.recognize(Capture.load(OCRTest.class, "test3.png")); 45 | System.out.println(str); 46 | 47 | // recognize using only one family set 48 | str = l.recognize(Capture.load(OCRTest.class, "test3.png"), "font_1"); 49 | System.out.println(str); 50 | 51 | // recognize using only one family set and rectangle 52 | ImageBinaryGrey i = new ImageBinaryGrey(Capture.load(OCRTest.class, "full.png")); 53 | str = l.recognize(i, 1285, 654, 1343, 677, l.getSymbols("font_1")); 54 | System.out.println(str); 55 | } 56 | } 57 | 58 | 59 | 60 | ## Lookup方法 61 | 62 | package com.github.axet.lookup; 63 | 64 | import java.awt.Point; 65 | import java.awt.image.BufferedImage; 66 | import java.util.Collections; 67 | import java.util.List; 68 | 69 | import com.github.axet.lookup.common.GFirst; 70 | import com.github.axet.lookup.common.GPoint; 71 | import com.github.axet.lookup.common.ImageBinaryGreyScale; 72 | 73 | public class SNCCTest { 74 | 75 | public static void main(String[] args) { 76 | BufferedImage image = Capture.load(OCRTest.class, "desktop.png"); 77 | BufferedImage templateSmall = Capture.load(OCRTest.class, "desktop_feature_small.png"); 78 | BufferedImage templateBig = Capture.load(OCRTest.class, "desktop_feature_big.png"); 79 | 80 | LookupScale s = new LookupScale(0.2f, 10, 0.65f, 0.95f); 81 | 82 | ImageBinaryGreyScale si = new ImageBinaryGreyScale(image); 83 | 84 | ImageBinaryGreyScale stBig = new ImageBinaryGreyScale(templateBig); 85 | ImageBinaryGreyScale stSmall = new ImageBinaryGreyScale(templateSmall); 86 | 87 | Long l; 88 | 89 | System.out.println("big"); 90 | l = System.currentTimeMillis(); 91 | { 92 | List pp = s.lookupAll(si, stBig); 93 | 94 | Collections.sort(pp, new GFirst()); 95 | 96 | for (GPoint p : pp) { 97 | System.out.println(p); 98 | } 99 | } 100 | System.out.println(System.currentTimeMillis() - l); 101 | 102 | System.out.println("small"); 103 | l = System.currentTimeMillis(); 104 | { 105 | List pp = s.lookupAll(si, stSmall); 106 | 107 | Collections.sort(pp, new GFirst()); 108 | 109 | for (GPoint p : pp) { 110 | System.out.println(p); 111 | } 112 | } 113 | System.out.println(System.currentTimeMillis() - l); 114 | 115 | System.out.println("big"); 116 | l = System.currentTimeMillis(); 117 | { 118 | List pp = s.lookupAll(si, stBig); 119 | 120 | Collections.sort(pp, new GFirst()); 121 | 122 | for (GPoint p : pp) { 123 | System.out.println(p); 124 | } 125 | } 126 | System.out.println(System.currentTimeMillis() - l); 127 | 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/main/java/info/hb/lookup/object/LookupColor.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object; 2 | 3 | import info.hb.lookup.object.Lookup.NotFound; 4 | import info.hb.lookup.object.common.ImageBinary; 5 | import info.hb.lookup.object.common.ImageBinaryChannel; 6 | import info.hb.lookup.object.common.ImageBinaryGrey; 7 | import info.hb.lookup.object.common.RangeColor; 8 | import info.hb.lookup.object.proc.NCC.WrongChannelType; 9 | 10 | import java.awt.Point; 11 | import java.awt.image.BufferedImage; 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | 15 | public class LookupColor { 16 | 17 | // 18 | // lookup 19 | // 20 | 21 | static boolean find(BufferedImage bi, int x, int y, BufferedImage icon, float m) { 22 | for (int yy = 0; yy < icon.getHeight(); yy++) { 23 | for (int xx = 0; xx < icon.getWidth(); xx++) { 24 | int rgb1 = icon.getRGB(xx, yy); 25 | int rgb2 = bi.getRGB(x + xx, y + yy); 26 | RangeColor r = new RangeColor(rgb1, m); 27 | if (!r.inRange(rgb2)) 28 | return false; 29 | } 30 | } 31 | 32 | return true; 33 | } 34 | 35 | public static boolean find(ImageBinary image, int x, int y, ImageBinary template, double m) { 36 | m = m * 255; 37 | for (int yy = 0; yy < template.getHeight(); yy++) { 38 | for (int xx = 0; xx < template.getWidth(); xx++) { 39 | List ci = image.getChannels(); 40 | List ct = template.getChannels(); 41 | 42 | int ii = Math.min(ci.size(), ct.size()); 43 | 44 | for (int i = 0; i < ii; i++) { 45 | ImageBinaryChannel cct = ct.get(i); 46 | ImageBinaryChannel cci = ci.get(i); 47 | 48 | if (!cct.type.equals(cci.type)) 49 | throw new WrongChannelType(); 50 | 51 | double rgb1 = cct.zeroMean.s(xx, yy); 52 | double rgb2 = cci.zeroMean.s(x + xx, y + yy); 53 | double min = rgb1 - m; 54 | double max = rgb1 + m; 55 | if (rgb2 < min || rgb2 > max) 56 | return false; 57 | } 58 | } 59 | } 60 | 61 | return true; 62 | } 63 | 64 | public static Point lookup(BufferedImage bi, BufferedImage icon) { 65 | return lookupUL(bi, icon, 0.10f); 66 | } 67 | 68 | public static Point lookupUL(BufferedImage image, BufferedImage template, float m) { 69 | return lookupUL(image, template, 0, 0, image.getWidth() - 1, image.getHeight() - 1, m); 70 | } 71 | 72 | public static Point lookupUL(BufferedImage image, BufferedImage template, int x1, int y1, int x2, int y2, float m) { 73 | for (int y = y1; y < y2 - template.getHeight(); y++) { 74 | for (int x = x1; x < x2 - template.getWidth(); x++) { 75 | if (find(image, x, y, template, m)) 76 | return new Point(x, y); 77 | } 78 | } 79 | 80 | return null; 81 | } 82 | 83 | public static Point lookupUL(ImageBinaryGrey image, ImageBinaryGrey template, float m) { 84 | for (int y = 0; y < image.getHeight() - template.getHeight(); y++) { 85 | for (int x = 0; x < image.getWidth() - template.getWidth(); x++) { 86 | if (find(image, x, y, template, m)) 87 | return new Point(x, y); 88 | } 89 | } 90 | 91 | return null; 92 | } 93 | 94 | public static List lookupAllUL(BufferedImage image, BufferedImage template, float m) { 95 | List list = new ArrayList<>(); 96 | 97 | for (int y = 0; y < image.getHeight() - template.getHeight(); y++) { 98 | for (int x = 0; x < image.getWidth() - template.getWidth(); x++) { 99 | if (find(image, x, y, template, m)) 100 | list.add(new Point(x, y)); 101 | } 102 | } 103 | 104 | return list; 105 | } 106 | 107 | /** 108 | * lookup center of image 109 | * 110 | * @param bi 111 | * @param exit 112 | * @param m 113 | * @return 114 | */ 115 | static public Point lookup(BufferedImage bi, BufferedImage exit, float m) { 116 | return lookup(bi, exit, 0, 0, bi.getWidth() - 1, bi.getHeight() - 1, m); 117 | } 118 | 119 | static public Point lookup(BufferedImage bi, BufferedImage exit, int x1, int y1, int x2, int y2, float m) { 120 | Point pul = lookupUL(bi, exit, x1, y1, x2, y2, m); 121 | if (pul == null) 122 | throw new NotFound(); 123 | 124 | int x = pul.x + exit.getWidth() / 2; 125 | int y = pul.y + exit.getHeight() / 2; 126 | return new Point(x, y); 127 | } 128 | 129 | static public Point lookupMeanImage(ImageBinaryGrey bi, ImageBinaryGrey i, int x1, int y1, int x2, int y2, float p) { 130 | Point pul = lookupUL(bi, i, p); 131 | if (pul == null) 132 | throw new NotFound(); 133 | 134 | int x = pul.x + i.getWidth() / 2; 135 | int y = pul.y + i.getHeight() / 2; 136 | return new Point(x, y); 137 | } 138 | 139 | static public List lookupAll(BufferedImage bi, BufferedImage i) { 140 | return lookupAll(bi, i, 0.10f); 141 | } 142 | 143 | static public List lookupAll(BufferedImage bi, BufferedImage i, float p) { 144 | return lookupAll(bi, i, 0, 0, bi.getWidth(), bi.getHeight(), p); 145 | } 146 | 147 | static public List lookupAll(BufferedImage bi, BufferedImage i, int x1, int y1, int x2, int y2, float p) { 148 | List pul = lookupAllUL(bi, i, p); 149 | if (pul.size() == 0) 150 | throw new NotFound(); 151 | 152 | for (Point pp : pul) { 153 | pp.x += i.getWidth() / 2; 154 | pp.y += i.getHeight() / 2; 155 | } 156 | 157 | return pul; 158 | } 159 | 160 | } 161 | -------------------------------------------------------------------------------- /src/main/java/info/hb/lookup/object/common/FeatureK.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object.common; 2 | 3 | import info.hb.lookup.object.Capture; 4 | 5 | import java.awt.Color; 6 | import java.awt.Graphics; 7 | import java.awt.image.BufferedImage; 8 | import java.util.ArrayList; 9 | import java.util.Arrays; 10 | import java.util.List; 11 | import java.util.Set; 12 | import java.util.TreeSet; 13 | 14 | public class FeatureK { 15 | 16 | public Feature f; 17 | public List list; 18 | public double k; 19 | 20 | public IntegralImage template; 21 | 22 | public FeatureK(RectK k) { 23 | list.add(k); 24 | } 25 | 26 | public FeatureK(Feature f, IntegralImage template) { 27 | this.template = template; 28 | this.f = f; 29 | 30 | Set list = new TreeSet(); 31 | for (int x = 0; x < f.cx; x++) { 32 | for (int y = 0; y < f.cy; y++) { 33 | RectK k = rectNearFill(x, y); 34 | list.add(k); 35 | } 36 | } 37 | this.list = Arrays.asList(list.toArray(new RectK[] {})); 38 | 39 | double dx = template.cx / (double) f.cx; 40 | double dy = template.cy / (double) f.cy; 41 | 42 | for (RectK k : this.list) { 43 | int w = k.x2 - k.x1 + 1; 44 | int h = k.y2 - k.y1 + 1; 45 | 46 | k.x1 *= dx; 47 | k.y1 *= dy; 48 | k.x2 = (int) (k.x1 + w * dx - 1); 49 | k.y2 = (int) (k.y1 + h * dy - 1); 50 | 51 | k.cxBase = template.cx; 52 | k.cyBase = template.cy; 53 | 54 | k.k = template.mean(k.x1, k.y1, k.x2, k.y2); 55 | 56 | this.k += k.k; 57 | } 58 | 59 | // this.list = fillFeature(3); 60 | } 61 | 62 | /** 63 | * debug. make feature exact as template image 64 | * 65 | * @param step 66 | * @return 67 | */ 68 | List fillFeature(int step) { 69 | List list = new ArrayList(); 70 | 71 | int cx = template.cx; 72 | int cy = template.cy; 73 | 74 | for (int x = 0; x < cx; x += step) { 75 | for (int y = 0; y < cy; y += step) { 76 | RectK k = new RectK(x, y); 77 | k.x2 += step - 1; 78 | k.y2 += step - 1; 79 | 80 | if (k.x2 >= cx) 81 | k.x2 = cx - 1; 82 | 83 | if (k.y2 >= cy) 84 | k.y2 = cy - 1; 85 | 86 | if (k.getWidth() == 0) 87 | continue; 88 | 89 | if (k.getHeight() == 0) 90 | continue; 91 | 92 | k.cxBase = template.cx; 93 | k.cyBase = template.cy; 94 | 95 | k.k = template.mean(k.x1, k.y1, k.x2, k.y2); 96 | 97 | list.add(k); 98 | } 99 | } 100 | 101 | return list; 102 | } 103 | 104 | RectK rectNearFill(int x, int y) { 105 | RectK k = near(x, y); 106 | k = fill(k); 107 | return k; 108 | } 109 | 110 | int limX(int i) { 111 | if (i < 0) 112 | return 0; 113 | 114 | if (i >= f.cx) 115 | return f.cx - 1; 116 | 117 | return i; 118 | } 119 | 120 | int limY(int i) { 121 | if (i < 0) 122 | return 0; 123 | 124 | if (i >= f.cy) 125 | return f.cy - 1; 126 | 127 | return i; 128 | } 129 | 130 | RectK near(int x, int y) { 131 | if (test(x, y)) 132 | return new RectK(x, y); 133 | 134 | int m = Math.max(f.cx, f.cy); 135 | for (int r = 1; r < m; r++) { 136 | int xx; 137 | int xxm; 138 | int yy; 139 | int yym; 140 | 141 | // top - bottom 142 | xx = limX(x - r); 143 | yym = limY(y + r); 144 | for (yy = limY(y - r); yy <= yym; yy++) { 145 | if (test(xx, yy)) 146 | return new RectK(xx, yy); 147 | } 148 | // left - right 149 | yy = limY(y + r); 150 | xxm = limX(x + r); 151 | for (xx = limX(x - r); xx <= xxm; xx++) { 152 | if (test(xx, yy)) 153 | return new RectK(xx, yy); 154 | } 155 | // down - top 156 | xx = limX(x + r); 157 | yym = limY(y - r); 158 | for (yy = limY(y + r); yy >= yym; yy--) { 159 | if (test(xx, yy)) 160 | return new RectK(xx, yy); 161 | } 162 | // right - left 163 | yy = limY(y - r); 164 | xxm = limX(x - r); 165 | for (xx = limX(x + r); xx >= xxm; xx--) { 166 | if (test(xx, yy)) 167 | return new RectK(xx, yy); 168 | } 169 | } 170 | 171 | throw new RuntimeException("no 1 found"); 172 | } 173 | 174 | boolean test(int x, int y) { 175 | if (x >= f.cx) 176 | return false; 177 | if (y >= f.cy) 178 | return false; 179 | 180 | if (f.s(x, y) == 1) 181 | return true; 182 | 183 | return false; 184 | } 185 | 186 | RectK fill(RectK k) { 187 | while (test(k.x1 - 1, k.y1)) { 188 | k.x1--; 189 | } 190 | 191 | while (test(k.x1, k.y1 - 1)) { 192 | k.y1--; 193 | } 194 | 195 | while (test(k.x2 + 1, k.y1)) { 196 | k.x2++; 197 | } 198 | 199 | while (test(k.x2, k.y2 + 1)) { 200 | k.y2++; 201 | } 202 | 203 | k.cxBase = f.cx; 204 | k.cyBase = f.cy; 205 | 206 | return k; 207 | } 208 | 209 | public void devide(RectK k) { 210 | ; 211 | } 212 | 213 | public BufferedImage getImage() { 214 | BufferedImage image = new BufferedImage(template.cx, template.cy, BufferedImage.TYPE_INT_ARGB); 215 | Graphics g = image.getGraphics(); 216 | 217 | g.setColor(Color.MAGENTA); 218 | g.fillRect(0, 0, template.cx, template.cy); 219 | 220 | g.setColor(new Color(0, 0, 0)); 221 | for (RectK k : list) { 222 | g.fillRect(k.x1, k.y1, k.getWidth(), k.getHeight()); 223 | } 224 | return image; 225 | } 226 | 227 | public void writeDesktop() { 228 | Capture.writeDesktop(getImage()); 229 | } 230 | 231 | } 232 | -------------------------------------------------------------------------------- /src/main/java/info/hb/lookup/object/LookupScale.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object; 2 | 3 | import info.hb.lookup.object.Lookup.NotFound; 4 | import info.hb.lookup.object.common.GFirst; 5 | import info.hb.lookup.object.common.GFirstLeftRight; 6 | import info.hb.lookup.object.common.GPoint; 7 | import info.hb.lookup.object.common.GSPoint; 8 | import info.hb.lookup.object.common.ImageBinary; 9 | import info.hb.lookup.object.common.ImageBinaryGreyScaleRGB; 10 | import info.hb.lookup.object.common.ImageBinaryScale; 11 | import info.hb.lookup.object.proc.NCC; 12 | 13 | import java.awt.Point; 14 | import java.awt.Rectangle; 15 | import java.awt.image.BufferedImage; 16 | import java.util.ArrayList; 17 | import java.util.Collections; 18 | import java.util.List; 19 | 20 | public class LookupScale { 21 | 22 | // minimum scale used 23 | public double s = 0; 24 | // blur kernel 25 | public int k; 26 | public float gg; 27 | public float g; 28 | 29 | /** 30 | * 31 | * @param scaleSize 32 | * ex:5 33 | * 34 | * @param blurKernel 35 | * ex:10 36 | * @param gg 37 | * ex:0.70f 38 | * @param g 39 | * ex:0.90f - for big templates, and 0.95f for small templates 40 | */ 41 | public LookupScale(double s, int blurKernel, float gg, float g) { 42 | this.s = s; 43 | this.k = blurKernel; 44 | this.gg = gg; 45 | this.g = g; 46 | } 47 | 48 | public GSPoint lookup(BufferedImage i, BufferedImage t) { 49 | List list = lookupAll(i, t); 50 | 51 | if (list.size() == 0) 52 | throw new NotFound(); 53 | 54 | Collections.sort(list, new GFirst()); 55 | 56 | return list.get(0); 57 | } 58 | 59 | public List lookupAll(BufferedImage i, BufferedImage t) { 60 | ImageBinaryScale templateBinary = new ImageBinaryGreyScaleRGB(t); 61 | ImageBinaryScale imageBinary = new ImageBinaryGreyScaleRGB(i); 62 | 63 | return lookupAll(imageBinary, templateBinary); 64 | } 65 | 66 | public GSPoint lookup(ImageBinaryScale image, ImageBinaryScale template) { 67 | List list = lookupAll(image, template); 68 | 69 | if (list.size() == 0) 70 | throw new NotFound(); 71 | 72 | Collections.sort(list, new GFirst()); 73 | 74 | return list.get(0); 75 | } 76 | 77 | public List lookupAll(ImageBinaryScale image, ImageBinaryScale template) { 78 | scale(image, template); 79 | 80 | return lookupAll(image, 0, 0, image.image.getWidth() - 1, image.image.getHeight() - 1, template); 81 | } 82 | 83 | public GSPoint lookup(ImageBinaryScale image, int x1, int y1, int x2, int y2, ImageBinaryScale template) { 84 | scale(image, template); 85 | 86 | List list = lookupAll(image, x1, y1, x2, y2, template); 87 | 88 | if (list.size() == 0) 89 | throw new NotFound(); 90 | 91 | Collections.sort(list, new GFirst()); 92 | 93 | return list.get(0); 94 | } 95 | 96 | public List lookupAll(ImageBinaryScale image, int x1, int y1, int x2, int y2, ImageBinaryScale template) { 97 | scale(image, template); 98 | 99 | int sx1 = (int) (x1 * s); 100 | int sy1 = (int) (y1 * s); 101 | int sx2 = (int) (x2 * s); 102 | int sy2 = (int) (y2 * s); 103 | 104 | List result = new ArrayList<>(); 105 | 106 | for (ImageBinary imageScaleBin : image.scales) { 107 | int ssy2 = sy2; 108 | int ssx2 = sx2; 109 | 110 | if (ssy2 >= imageScaleBin.getHeight()) 111 | ssy2 = imageScaleBin.getHeight() - 1; 112 | if (ssx2 >= imageScaleBin.getWidth()) 113 | ssx2 = imageScaleBin.getWidth() - 1; 114 | 115 | for (ImageBinary templateScaleBin : template.scales) { 116 | List list = NCC.lookupAll(imageScaleBin, sx1, sy1, ssx2, ssy2, templateScaleBin, gg); 117 | 118 | int mx = (int) (1 / s) * 2; 119 | int my = (int) (1 / s) * 2; 120 | 121 | for (GPoint p : list) { 122 | Point p1 = new Point(p); 123 | 124 | p1.x = (int) (p1.x / s - mx); 125 | p1.y = (int) (p1.y / s - mx); 126 | 127 | Point p2 = new Point(p1); 128 | p2.x = template.image.getWidth() - 1 + p2.x + 2 * mx; 129 | p2.y = template.image.getHeight() - 1 + p2.y + 2 * my; 130 | 131 | if (p2.x >= image.image.getWidth()) 132 | p2.x = image.image.getWidth() - 1; 133 | if (p2.y >= image.image.getHeight()) 134 | p2.y = image.image.getHeight() - 1; 135 | 136 | List list2 = NCC.lookupAll(image.image, p1.x, p1.y, p2.x, p2.y, template.image, g); 137 | 138 | for (GPoint g : list2) { 139 | result.add(new GSPoint(g, p.g)); 140 | } 141 | } 142 | 143 | // we found something stop looking 144 | if (result.size() != 0) 145 | break; 146 | } 147 | } 148 | 149 | // delete duplicates 150 | Collections.sort(result, new GFirstLeftRight(template.image)); 151 | for (int k = 0; k < result.size(); k++) { 152 | GPoint kk = result.get(k); 153 | for (int j = k + 1; j < result.size(); j++) { 154 | GPoint jj = result.get(j); 155 | if (cross(template.image, kk, jj)) { 156 | result.remove(jj); 157 | j--; 158 | } 159 | } 160 | } 161 | 162 | for (GPoint p : result) { 163 | p.x += template.image.getWidth() / 2; 164 | p.y += template.image.getHeight() / 2; 165 | } 166 | 167 | return result; 168 | } 169 | 170 | boolean cross(ImageBinary image, GPoint i1, GPoint i2) { 171 | Rectangle r1 = new Rectangle(i1.x, i1.y, image.getWidth(), image.getHeight()); 172 | Rectangle r2 = new Rectangle(i2.x, i2.y, image.getWidth(), image.getHeight()); 173 | return r1.intersects(r2); 174 | } 175 | 176 | void scale(ImageBinaryScale image, ImageBinaryScale template) { 177 | if (s == 0) { 178 | s = template.s; 179 | } 180 | 181 | if (s != template.s) { 182 | template.rescaleMosaic(s, k); 183 | } 184 | 185 | if (s != image.s) { 186 | image.rescale(s, k); 187 | } 188 | } 189 | 190 | } 191 | -------------------------------------------------------------------------------- /src/main/java/info/hb/lookup/object/proc/NCC.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object.proc; 2 | 3 | import info.hb.lookup.object.Lookup.NotFound; 4 | import info.hb.lookup.object.common.GFirst; 5 | import info.hb.lookup.object.common.GPoint; 6 | import info.hb.lookup.object.common.ImageBinary; 7 | import info.hb.lookup.object.common.ImageBinaryChannel; 8 | import info.hb.lookup.object.common.ImageBinaryRGB; 9 | import info.hb.lookup.object.common.ImageMultiplySum; 10 | 11 | import java.awt.image.BufferedImage; 12 | import java.util.ArrayList; 13 | import java.util.Collections; 14 | import java.util.List; 15 | 16 | /** 17 | * 18 | * http://www.fmwconcepts.com/imagemagick/similar/index.php 19 | * 20 | * 1) mean && stddev 21 | * 22 | * 2) image1(x,y) - mean1 && image2(x,y) - mean2 23 | * 24 | * 3) [3] = (image1(x,y) - mean)(x,y) * (image2(x,y) - mean)(x,y) 25 | * 26 | * 4) [4] = mean([3]) 27 | * 28 | * 5) [4] / (stddev1 * stddev2) 29 | * 30 | * Normalized cross correlation algorithm 31 | */ 32 | public class NCC { 33 | 34 | public static class WrongChannelType extends RuntimeException { 35 | 36 | private static final long serialVersionUID = 2487329823855153614L; 37 | 38 | } 39 | 40 | static public GPoint lookup(BufferedImage i, BufferedImage t, float m) { 41 | List list = lookupAll(i, t, m); 42 | 43 | if (list.size() == 0) 44 | throw new NotFound(); 45 | 46 | Collections.sort(list, new GFirst()); 47 | 48 | return list.get(0); 49 | } 50 | 51 | static public List lookupAll(BufferedImage i, BufferedImage t, float m) { 52 | ImageBinaryRGB imageBinary = new ImageBinaryRGB(i); 53 | ImageBinaryRGB templateBinary = new ImageBinaryRGB(t); 54 | 55 | return lookupAll(imageBinary, templateBinary, m); 56 | } 57 | 58 | static public GPoint lookup(ImageBinary image, ImageBinary template, float m) { 59 | List list = lookupAll(image, template, m); 60 | 61 | if (list.size() == 0) 62 | throw new NotFound(); 63 | 64 | Collections.sort(list, new GFirst()); 65 | 66 | return list.get(0); 67 | } 68 | 69 | static public List lookupAll(ImageBinary image, ImageBinary template, float m) { 70 | return lookupAll(image, 0, 0, image.getWidth() - 1, image.getHeight() - 1, template, m); 71 | } 72 | 73 | static public GPoint lookup(ImageBinary image, int x1, int y1, int x2, int y2, ImageBinary template, float m) { 74 | List list = lookupAll(image, x1, y1, x2, y2, template, m); 75 | 76 | if (list.size() == 0) 77 | throw new NotFound(); 78 | 79 | Collections.sort(list, new GFirst()); 80 | 81 | return list.get(0); 82 | } 83 | 84 | static public List lookupAll(ImageBinary image, int x1, int y1, int x2, int y2, ImageBinary template, 85 | float m) { 86 | List list = new ArrayList<>(); 87 | 88 | for (int x = x1; x <= x2 - template.getWidth() + 1; x++) { 89 | for (int y = y1; y <= y2 - template.getHeight() + 1; y++) { 90 | GPoint g = lookup(image, template, x, y, m); 91 | if (g != null) 92 | list.add(g); 93 | } 94 | } 95 | 96 | return list; 97 | } 98 | 99 | static double numerator(ImageBinaryChannel image, ImageBinaryChannel template, int xx, int yy) { 100 | ImageMultiplySum m = new ImageMultiplySum(image.zeroMean, xx, yy, template.zeroMean); 101 | return m.sum; 102 | } 103 | 104 | static double denominator(ImageBinaryChannel image, ImageBinaryChannel template, int xx, int yy) { 105 | double di = image.dev2n(xx, yy, xx + template.getWidth() - 1, yy + template.getHeight() - 1); 106 | double dt = template.dev2n(); 107 | return Math.sqrt(di * dt); 108 | } 109 | 110 | static public GPoint lookup(ImageBinary image, ImageBinary template, int x, int y, float m) { 111 | List ci = image.getChannels(); 112 | List ct = template.getChannels(); 113 | 114 | int ii = Math.min(ci.size(), ct.size()); 115 | 116 | double g = Double.MAX_VALUE; 117 | 118 | for (int i = 0; i < ii; i++) { 119 | ImageBinaryChannel cct = ct.get(i); 120 | ImageBinaryChannel cci = ci.get(i); 121 | 122 | if (!cct.type.equals(cci.type)) 123 | throw new WrongChannelType(); 124 | 125 | double gg = gamma(cci, cct, x, y); 126 | 127 | if (gg < m) 128 | return null; 129 | 130 | g = Math.min(g, gg); 131 | } 132 | 133 | return new GPoint(x, y, g); 134 | } 135 | 136 | static public double gamma(ImageBinary image, ImageBinary template, int x, int y) { 137 | List ci = image.getChannels(); 138 | List ct = template.getChannels(); 139 | 140 | int ii = Math.min(ci.size(), ct.size()); 141 | 142 | double g = 0; 143 | 144 | for (int i = 0; i < ii; i++) { 145 | ImageBinaryChannel cct = ct.get(i); 146 | ImageBinaryChannel cci = ci.get(i); 147 | 148 | if (!cct.type.equals(cci.type)) 149 | throw new WrongChannelType(); 150 | 151 | g += gamma(cci, cct, x, y); 152 | } 153 | 154 | g /= ii; 155 | 156 | return g; 157 | } 158 | 159 | static public double gammaMin(ImageBinary image, ImageBinary template, int x, int y) { 160 | List ci = image.getChannels(); 161 | List ct = template.getChannels(); 162 | 163 | int ii = Math.min(ci.size(), ct.size()); 164 | 165 | double g = Double.MAX_VALUE; 166 | 167 | for (int i = 0; i < ii; i++) { 168 | ImageBinaryChannel cct = ct.get(i); 169 | ImageBinaryChannel cci = ci.get(i); 170 | 171 | if (!cct.type.equals(cci.type)) 172 | throw new WrongChannelType(); 173 | 174 | g = Math.min(g, gamma(cci, cct, x, y)); 175 | } 176 | 177 | return g; 178 | } 179 | 180 | static public double gamma(ImageBinaryChannel image, ImageBinaryChannel template, int xx, int yy) { 181 | double d = denominator(image, template, xx, yy); 182 | 183 | if (d == 0) 184 | return -1; 185 | 186 | double n = numerator(image, template, xx, yy); 187 | 188 | return (n / d); 189 | } 190 | 191 | } 192 | -------------------------------------------------------------------------------- /src/main/java/info/hb/lookup/object/OCR.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object; 2 | 3 | import info.hb.lookup.object.common.ClassResources; 4 | import info.hb.lookup.object.common.FontFamily; 5 | import info.hb.lookup.object.common.FontSymbol; 6 | import info.hb.lookup.object.common.FontSymbolLookup; 7 | import info.hb.lookup.object.common.ImageBinary; 8 | import info.hb.lookup.object.common.ImageBinaryGrey; 9 | 10 | import java.awt.image.BufferedImage; 11 | import java.io.File; 12 | import java.io.InputStream; 13 | import java.io.UnsupportedEncodingException; 14 | import java.net.URLDecoder; 15 | import java.util.Collections; 16 | import java.util.List; 17 | 18 | import org.apache.commons.io.FilenameUtils; 19 | 20 | public class OCR extends OCRCore { 21 | 22 | public OCR(float threshold) { 23 | super(threshold); 24 | } 25 | 26 | /** 27 | * set sensitivity 28 | * 29 | * @param threshold 30 | * 1 - exact match. 0 - not match. -1 - opposite difference 31 | */ 32 | public void setThreshold(float threshold) { 33 | this.threshold = threshold; 34 | } 35 | 36 | public float getThreshold() { 37 | return threshold; 38 | } 39 | 40 | /** 41 | * Load fonts / symbols from a class directory or jar file 42 | * 43 | * @param c 44 | * class name, corresponded to the resources. com.example.MyApp.class 45 | * @param path 46 | * path to the fonts folder. directory should only contain folders with fonts which to load 47 | * 48 | */ 49 | public void loadFontsDirectory(Class c, File path) { 50 | ClassResources e = new ClassResources(c, path); 51 | 52 | List str = e.names(); 53 | 54 | for (String s : str) 55 | loadFont(c, new File(path, s)); 56 | } 57 | 58 | /** 59 | * Load specified font family to load 60 | * 61 | * @param c 62 | * class name, corresponded to the resources. com.example.MyApp.class 63 | * @param path 64 | * path to the fonts folder. directory should only contain folders with fonts which to load. 65 | * @param name 66 | * name of the font to load 67 | * 68 | */ 69 | public void loadFont(Class c, File path) { 70 | ClassResources e = new ClassResources(c, path); 71 | 72 | List str = e.names(); 73 | 74 | for (String s : str) { 75 | File f = new File(path, s); 76 | 77 | InputStream is = c.getResourceAsStream(f.getPath()); 78 | 79 | String symbol = FilenameUtils.getBaseName(s); 80 | 81 | try { 82 | symbol = URLDecoder.decode(symbol, "UTF-8"); 83 | } catch (UnsupportedEncodingException ee) { 84 | throw new RuntimeException(ee); 85 | } 86 | 87 | String name = path.getName(); 88 | loadFontSymbol(name, symbol, is); 89 | } 90 | } 91 | 92 | public void loadFontSymbol(String fontName, String fontSymbol, InputStream is) { 93 | FontFamily ff = fontFamily.get(fontName); 94 | if (ff == null) { 95 | ff = new FontFamily(fontName); 96 | fontFamily.put(fontName, ff); 97 | } 98 | FontSymbol f = loadFontSymbol(ff, fontSymbol, is); 99 | ff.add(f); 100 | } 101 | 102 | public FontSymbol loadFontSymbol(FontFamily ff, String fontSymbol, InputStream is) { 103 | BufferedImage bi = Capture.load(is); 104 | return new FontSymbol(ff, fontSymbol, bi); 105 | } 106 | 107 | public String recognize(BufferedImage bi) { 108 | ImageBinary i = new ImageBinaryGrey(bi); 109 | 110 | return recognize(i); 111 | } 112 | 113 | public String recognize(ImageBinary i) { 114 | List list = getSymbols(); 115 | 116 | return recognize(i, 0, 0, i.getWidth() - 1, i.getHeight() - 1, list); 117 | } 118 | 119 | /** 120 | * 121 | * @param fontSet 122 | * use font in the specified folder only 123 | * @param bi 124 | * @return 125 | */ 126 | public String recognize(BufferedImage bi, String fontSet) { 127 | ImageBinary i = new ImageBinaryGrey(bi); 128 | 129 | return recognize(i, fontSet); 130 | } 131 | 132 | public String recognize(ImageBinary i, String fontSet) { 133 | List list = getSymbols(fontSet); 134 | 135 | return recognize(i, 0, 0, i.getWidth() - 1, i.getHeight() - 1, list); 136 | } 137 | 138 | public String recognize(ImageBinary i, int x1, int y1, int x2, int y2) { 139 | List list = getSymbols(); 140 | 141 | return recognize(i, x1, y1, x2, y2, list); 142 | } 143 | 144 | public String recognize(ImageBinary i, int x1, int y1, int x2, int y2, String fontFamily) { 145 | List list = getSymbols(fontFamily); 146 | 147 | return recognize(i, x1, y1, x2, y2, list); 148 | } 149 | 150 | public String recognize(ImageBinary i, int x1, int y1, int x2, int y2, List list) { 151 | List all = findAll(list, i, x1, y1, x2, y2); 152 | 153 | return recognize(all); 154 | } 155 | 156 | public String recognize(List all) { 157 | String str = ""; 158 | 159 | if (all.size() == 0) 160 | return str; 161 | 162 | // bigger first. 163 | Collections.sort(all, new BiggerFirst(all)); 164 | 165 | // big images eat small ones 166 | for (int k = 0; k < all.size(); k++) { 167 | FontSymbolLookup kk = all.get(k); 168 | for (int j = k + 1; j < all.size(); j++) { 169 | FontSymbolLookup jj = all.get(j); 170 | if (kk.cross(jj)) { 171 | all.remove(jj); 172 | j--; 173 | } 174 | } 175 | } 176 | 177 | // sort top/bottom/left/right 178 | Collections.sort(all, new Left2Right()); 179 | 180 | // calculate rows 181 | { 182 | int x = all.get(0).x; 183 | int cx = 0; 184 | for (FontSymbolLookup s : all) { 185 | int maxCX = Math.max(cx, s.getWidth()); 186 | 187 | // if distance betten end of previous symbol and begining of the 188 | // current is larger then a char size, then it is a space 189 | if (s.x - (x + cx) >= maxCX) 190 | str += " "; 191 | 192 | // if we drop back, then we have a end of line 193 | if (s.x < x) 194 | str += "\n"; 195 | x = s.x + s.getWidth(); 196 | cx = s.getWidth(); 197 | str += s.fs.fontSymbol; 198 | } 199 | } 200 | 201 | return str; 202 | } 203 | 204 | } 205 | -------------------------------------------------------------------------------- /src/main/java/info/hb/lookup/object/Capture.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object; 2 | 3 | import java.awt.Color; 4 | import java.awt.Dimension; 5 | import java.awt.Graphics; 6 | import java.awt.Graphics2D; 7 | import java.awt.GraphicsDevice; 8 | import java.awt.GraphicsEnvironment; 9 | import java.awt.Rectangle; 10 | import java.awt.Robot; 11 | import java.awt.Toolkit; 12 | import java.awt.image.BufferedImage; 13 | import java.awt.image.ColorModel; 14 | import java.io.File; 15 | import java.io.InputStream; 16 | 17 | import javax.imageio.ImageIO; 18 | import javax.swing.ImageIcon; 19 | 20 | import org.apache.commons.io.FileUtils; 21 | 22 | import com.github.axet.desktop.Desktop; 23 | import com.github.axet.desktop.DesktopFolders; 24 | 25 | public class Capture { 26 | 27 | public static ColorModel getColorModel() { 28 | GraphicsDevice d = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice(); 29 | return d.getDefaultConfiguration().getColorModel(); 30 | } 31 | 32 | public static GraphicsDevice getScreenDevice(int screenNumber) throws Exception { 33 | GraphicsDevice[] screens = getScreenDevices(); 34 | if (screenNumber >= screens.length) { 35 | throw new Exception("CanvasFrame Error: Screen number " + screenNumber + " not found. " + "There are only " 36 | + screens.length + " screens."); 37 | } 38 | return screens[screenNumber];// .getDefaultConfiguration(); 39 | } 40 | 41 | public static GraphicsDevice[] getScreenDevices() { 42 | return GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices(); 43 | } 44 | 45 | // 46 | // capture 47 | // 48 | 49 | static public BufferedImage capture() { 50 | try { 51 | Dimension size = Toolkit.getDefaultToolkit().getScreenSize(); 52 | return capture(new Rectangle(size)); 53 | } catch (Exception e) { 54 | throw new RuntimeException(e); 55 | } 56 | } 57 | 58 | static public BufferedImage capture(Rectangle rec) { 59 | try { 60 | Robot robot = new Robot(); 61 | BufferedImage img = robot.createScreenCapture(rec); 62 | return img; 63 | } catch (Exception e) { 64 | throw new RuntimeException(e); 65 | } 66 | } 67 | 68 | // 69 | // crop 70 | // 71 | 72 | static public BufferedImage crop(BufferedImage image, int boundsSize) { 73 | Rectangle r = new Rectangle(image.getWidth(), image.getHeight()); 74 | r.x += boundsSize; 75 | r.y += boundsSize; 76 | r.width -= boundsSize * 2; 77 | r.height -= boundsSize * 2; 78 | return crop(image, r); 79 | } 80 | 81 | static public BufferedImage crop(BufferedImage image, Rectangle r) { 82 | return crop(image, r.x, r.y, r.x + r.width, r.y + r.height); 83 | } 84 | 85 | static public BufferedImage crop(BufferedImage src, int x1, int y1, int x2, int y2) { 86 | BufferedImage dest = new BufferedImage(x2 - x1, y2 - y1, src.getType()); 87 | Graphics g = dest.getGraphics(); 88 | g.drawImage(src, 0, 0, dest.getWidth(), dest.getHeight(), x1, y1, x1 + dest.getWidth(), y1 + dest.getHeight(), 89 | null); 90 | g.dispose(); 91 | 92 | return dest; 93 | } 94 | 95 | static public BufferedImage fill(BufferedImage image, Rectangle r) { 96 | BufferedImage bufferedImage = new BufferedImage(image.getWidth(null), image.getHeight(null), 97 | BufferedImage.TYPE_INT_RGB); 98 | Graphics2D g2 = bufferedImage.createGraphics(); 99 | g2.drawImage(image, null, null); 100 | g2.setColor(new Color(0xff0000ff)); 101 | g2.fillRect(r.x, r.y, r.width, r.height); 102 | return bufferedImage; 103 | } 104 | 105 | // 106 | // load / save 107 | // 108 | 109 | static public void writeDesktop(BufferedImage img, Rectangle rr, String file) { 110 | BufferedImage i = new BufferedImage(rr.width, rr.height, BufferedImage.TYPE_INT_ARGB); 111 | Graphics g = i.getGraphics(); 112 | g.drawImage(img, 0, 0, rr.width, rr.height, rr.x, rr.y, rr.x + rr.width, rr.y + rr.height, null); 113 | g.dispose(); 114 | 115 | writeDesktop(i, file); 116 | } 117 | 118 | static public void writeDesktop(BufferedImage img) { 119 | writeDesktop(img, Long.toString(System.currentTimeMillis()) + ".png"); 120 | } 121 | 122 | static public void writeDesktop(BufferedImage img, String file) { 123 | DesktopFolders d = Desktop.getDesktopFolders(); 124 | File f = FileUtils.getFile(d.getDesktop(), file); 125 | write(img, f); 126 | } 127 | 128 | static public void write(BufferedImage img, File f) { 129 | try { 130 | ImageIO.write(img, "PNG", f); 131 | } catch (Exception e) { 132 | throw new RuntimeException(e); 133 | } 134 | } 135 | 136 | static public BufferedImage load(File path, Rectangle rect) { 137 | BufferedImage src = load(path); 138 | 139 | BufferedImage dest = new BufferedImage((int) rect.getWidth(), (int) rect.getHeight(), src.getType()); 140 | Graphics g = dest.getGraphics(); 141 | g.drawImage(src, 0, 0, (int) rect.getWidth(), (int) rect.getHeight(), (int) rect.getX(), (int) rect.getY(), 142 | (int) rect.getX() + (int) rect.getWidth(), (int) rect.getY() + (int) rect.getHeight(), null); 143 | g.dispose(); 144 | 145 | return dest; 146 | } 147 | 148 | // easy call while debuging 149 | static public BufferedImage load(String path) { 150 | return load(new File(path)); 151 | } 152 | 153 | static public BufferedImage load(File path) { 154 | BufferedImage img = null; 155 | try { 156 | img = ImageIO.read(path); 157 | return img; 158 | } catch (Exception e) { 159 | throw new RuntimeException(e); 160 | } 161 | } 162 | 163 | static public BufferedImage load(Class c, String path) { 164 | return load(c.getResourceAsStream(path)); 165 | } 166 | 167 | static public BufferedImage load(InputStream path) { 168 | BufferedImage img = null; 169 | try { 170 | img = ImageIO.read(path); 171 | return img; 172 | } catch (Exception e) { 173 | throw new RuntimeException(e); 174 | } 175 | } 176 | 177 | static BufferedImage loadImageIcon(String path) { 178 | ImageIcon icon = new ImageIcon(path); 179 | BufferedImage bi = new BufferedImage(icon.getIconWidth(), icon.getIconHeight(), BufferedImage.TYPE_INT_ARGB); 180 | Graphics g = bi.createGraphics(); 181 | icon.paintIcon(null, g, 0, 0); 182 | g.dispose(); 183 | return bi; 184 | } 185 | 186 | } 187 | -------------------------------------------------------------------------------- /src/main/java/info/hb/lookup/object/Lookup.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object; 2 | 3 | import info.hb.lookup.object.proc.CannyEdgeDetector; 4 | 5 | import java.awt.Graphics; 6 | import java.awt.Graphics2D; 7 | import java.awt.Image; 8 | import java.awt.color.ColorSpace; 9 | import java.awt.image.BufferedImage; 10 | import java.awt.image.ColorConvertOp; 11 | import java.awt.image.ConvolveOp; 12 | import java.awt.image.Kernel; 13 | 14 | import com.jhlabs.image.GaussianFilter; 15 | 16 | public class Lookup { 17 | 18 | public static class NotFound extends RuntimeException { 19 | 20 | private static final long serialVersionUID = 5393563026702192412L; 21 | 22 | public NotFound() { 23 | super("NotFound"); 24 | } 25 | 26 | public NotFound(String s) { 27 | super(s); 28 | } 29 | 30 | } 31 | 32 | // convert 33 | 34 | public BufferedImage convert(Image image) { 35 | BufferedImage bufferedImage = new BufferedImage(image.getWidth(null), image.getHeight(null), 36 | BufferedImage.TYPE_INT_RGB); 37 | Graphics2D g2 = bufferedImage.createGraphics(); 38 | g2.drawImage(image, null, null); 39 | return bufferedImage; 40 | } 41 | 42 | // filter 43 | 44 | static public BufferedImage edgeImageDouble(BufferedImage b) { 45 | // b = Lookup.filterResizeDoubleCanvas(b); 46 | 47 | b = Lookup.edge(b); 48 | 49 | b = Lookup.filterRemoveCanvas(b); 50 | 51 | return b; 52 | } 53 | 54 | static public BufferedImage edgeImageCrop(BufferedImage b) { 55 | b = filterDoubleCanvas(b); 56 | 57 | b = Lookup.filterRemoveCanvas(b); 58 | 59 | return b; 60 | } 61 | 62 | static public BufferedImage filterSimply(BufferedImage bi) { 63 | BufferedImage buff = new BufferedImage(bi.getWidth(), bi.getHeight(), bi.getType()); 64 | 65 | float n = 1f / 25f; 66 | Kernel kernel = new Kernel(5, 5, new float[] { n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, 67 | n, n, n, n }); 68 | 69 | ConvolveOp op = new ConvolveOp(kernel); 70 | op.filter(bi, buff); 71 | 72 | return buff; 73 | } 74 | 75 | public BufferedImage filterResize(BufferedImage bi) { 76 | int cx = bi.getWidth() / 7; 77 | int cy = bi.getHeight() / 7; 78 | BufferedImage resizedImage = new BufferedImage(cx, cy, bi.getType()); 79 | Graphics2D g = resizedImage.createGraphics(); 80 | g.drawImage(bi, 0, 0, cx, cy, null); 81 | g.dispose(); 82 | 83 | return resizedImage; 84 | } 85 | 86 | static public BufferedImage filterResizeDouble(BufferedImage bi) { 87 | int cx = bi.getWidth() * 4; 88 | int cy = bi.getHeight() * 4; 89 | BufferedImage resizedImage = new BufferedImage(cx, cy, bi.getType()); 90 | Graphics2D g = resizedImage.createGraphics(); 91 | g.drawImage(bi, 0, 0, cx, cy, null); 92 | g.dispose(); 93 | return resizedImage; 94 | } 95 | 96 | static public BufferedImage scale(BufferedImage bi, double s, int blurKernel) { 97 | return scale(bi, s, blurKernel, 0, 0); 98 | } 99 | 100 | static public BufferedImage scale(BufferedImage bi, double s, int blurKernel, int px, int py) { 101 | bi = filterGausBlur(bi, blurKernel); 102 | 103 | int cx = (int) (bi.getWidth() * s) + px; 104 | int cy = (int) (bi.getHeight() * s) + py; 105 | 106 | Image src = bi.getScaledInstance(cx, cy, Image.SCALE_SMOOTH); 107 | 108 | BufferedImage resizedImage = new BufferedImage(cx, cy, bi.getType()); 109 | Graphics2D g = resizedImage.createGraphics(); 110 | g.drawImage(src, 0, 0, cx, cy, null); 111 | g.dispose(); 112 | 113 | return resizedImage; 114 | } 115 | 116 | static public BufferedImage scalePower(BufferedImage bi, double s) { 117 | double m = 1 / s; 118 | 119 | int cx = (int) (bi.getWidth() / m) + 1; 120 | int cy = (int) (bi.getHeight() / m) + 1; 121 | 122 | BufferedImage resizedImage = new BufferedImage(cx, cy, bi.getType()); 123 | for (int x = 0; x < bi.getWidth(); x += m) { 124 | for (int y = 0; y < bi.getHeight(); y += m) { 125 | resizedImage.setRGB((int) (x / m), (int) (y / m), bi.getRGB(x, y)); 126 | } 127 | } 128 | return resizedImage; 129 | } 130 | 131 | static public BufferedImage filterDoubleCanvas(BufferedImage bi) { 132 | int cx = bi.getWidth() * 2; 133 | int cy = bi.getHeight() * 2; 134 | 135 | BufferedImage resizedImage = new BufferedImage(cx, cy, bi.getType()); 136 | Graphics g = resizedImage.getGraphics(); 137 | g.drawImage(bi, cx / 4, cy / 4, bi.getWidth(), bi.getHeight(), null); 138 | g.dispose(); 139 | 140 | return resizedImage; 141 | } 142 | 143 | static public BufferedImage filterRemoveDoubleCanvas(BufferedImage bi) { 144 | int cx = bi.getWidth() / 2; 145 | int cy = bi.getHeight() / 2; 146 | 147 | BufferedImage resizedImage = new BufferedImage(cx, cy, bi.getType()); 148 | Graphics2D g = resizedImage.createGraphics(); 149 | g.drawImage(bi, 0, 0, cx, cy, cx / 2, cy / 2, cx / 2 + cx, cy / 2 + cy, null); 150 | g.dispose(); 151 | 152 | return resizedImage; 153 | } 154 | 155 | static public BufferedImage filterRemoveBorder(BufferedImage bi, int border) { 156 | int cx = bi.getWidth() - border * 2; 157 | int cy = bi.getHeight() - border * 2; 158 | 159 | BufferedImage resizedImage = new BufferedImage(cx, cy, bi.getType()); 160 | Graphics2D g = resizedImage.createGraphics(); 161 | g.drawImage(bi, 0, 0, cx, cy, border, border, border + cx, border + cy, null); 162 | g.dispose(); 163 | 164 | return resizedImage; 165 | } 166 | 167 | static public BufferedImage filterRemoveCanvas(BufferedImage bi) { 168 | int x1 = bi.getWidth(); 169 | int x2 = 0; 170 | 171 | int y1 = bi.getHeight(); 172 | int y2 = 0; 173 | 174 | for (int x = 0; x < bi.getWidth(); x++) { 175 | for (int y = 0; y < bi.getHeight(); y++) { 176 | if ((bi.getRGB(x, y) & 0xffffff) != 0) { 177 | if (x1 > x) 178 | x1 = x; 179 | if (x2 < x) 180 | x2 = x; 181 | if (y1 > y) 182 | y1 = y; 183 | if (y2 < y) 184 | y2 = y; 185 | } 186 | } 187 | } 188 | 189 | int cx = x2 - x1; 190 | int cy = y2 - y1; 191 | 192 | BufferedImage dest = new BufferedImage(cx, cy, bi.getType()); 193 | Graphics g = dest.getGraphics(); 194 | g.drawImage(bi, 0, 0, dest.getWidth(), dest.getHeight(), x1, y1, x1 + dest.getWidth(), y1 + dest.getHeight(), 195 | null); 196 | g.dispose(); 197 | 198 | return dest; 199 | } 200 | 201 | static public BufferedImage filterBlur(BufferedImage bi, int blurKernel) { 202 | BufferedImage buff = new BufferedImage(bi.getWidth(), bi.getHeight(), bi.getType()); 203 | 204 | float[] f = new float[blurKernel * blurKernel]; 205 | float n = 1 / (float) f.length; 206 | for (int i = 0; i < f.length; i++) 207 | f[i] = n; 208 | 209 | Kernel kernel = new Kernel(blurKernel, blurKernel, f); 210 | 211 | ConvolveOp op = new ConvolveOp(kernel, ConvolveOp.EDGE_NO_OP, null); 212 | op.filter(bi, buff); 213 | 214 | return buff; 215 | } 216 | 217 | static public BufferedImage filterGausBlur(BufferedImage bi, int blurKernel) { 218 | BufferedImage buff = new BufferedImage(bi.getWidth(), bi.getHeight(), bi.getType()); 219 | 220 | GaussianFilter op = new GaussianFilter(blurKernel); 221 | op.filter(bi, buff); 222 | 223 | return buff; 224 | } 225 | 226 | static public BufferedImage filterBlur3(BufferedImage bi) { 227 | BufferedImage buff = new BufferedImage(bi.getWidth(), bi.getHeight(), bi.getType()); 228 | 229 | float n = 1f / 9f; 230 | Kernel kernel = new Kernel(3, 3, new float[] { n, n, n, n, n, n, n, n, n }); 231 | 232 | ConvolveOp op = new ConvolveOp(kernel, ConvolveOp.EDGE_NO_OP, null); 233 | op.filter(bi, buff); 234 | 235 | return buff; 236 | } 237 | 238 | static public BufferedImage filterBlur5(BufferedImage bi) { 239 | BufferedImage buff = new BufferedImage(bi.getWidth(), bi.getHeight(), bi.getType()); 240 | 241 | float n = 1f / 25f; 242 | Kernel kernel = new Kernel(5, 5, new float[] { n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, 243 | n, n, n, n }); 244 | 245 | ConvolveOp op = new ConvolveOp(kernel); 246 | op.filter(bi, buff); 247 | 248 | return buff; 249 | } 250 | 251 | static public BufferedImage filterBlur8(BufferedImage bi) { 252 | BufferedImage buff = new BufferedImage(bi.getWidth(), bi.getHeight(), bi.getType()); 253 | 254 | float n = 1f / 64f; 255 | Kernel kernel = new Kernel(8, 8, new float[] { n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, 256 | n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, 257 | n, n, n, n, n, n, n, n, n, }); 258 | 259 | ConvolveOp op = new ConvolveOp(kernel); 260 | op.filter(bi, buff); 261 | 262 | return buff; 263 | } 264 | 265 | static public BufferedImage filterBlur10(BufferedImage bi) { 266 | BufferedImage buff = new BufferedImage(bi.getWidth(), bi.getHeight(), bi.getType()); 267 | 268 | float n = 1f / 100f; 269 | Kernel kernel = new Kernel(10, 10, new float[] { n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, 270 | n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, 271 | n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, 272 | n, n, n, n, n, n, n, n, n, n, n }); 273 | 274 | ConvolveOp op = new ConvolveOp(kernel); 275 | op.filter(bi, buff); 276 | 277 | return buff; 278 | } 279 | 280 | static public BufferedImage toGray(BufferedImage bi) { 281 | BufferedImage out = new BufferedImage(bi.getWidth(), bi.getHeight(), BufferedImage.TYPE_BYTE_GRAY); 282 | 283 | ColorConvertOp op = new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_GRAY), null); 284 | op.filter(bi, out); 285 | 286 | return out; 287 | } 288 | 289 | static public BufferedImage edge(BufferedImage bi) { 290 | CannyEdgeDetector detector = new CannyEdgeDetector(); 291 | 292 | detector.setLowThreshold(3f); 293 | detector.setHighThreshold(3f); 294 | detector.setGaussianKernelWidth(2); 295 | detector.setGaussianKernelRadius(1f); 296 | 297 | detector.setSourceImage(bi); 298 | detector.process(); 299 | 300 | return detector.getEdgesImage(); 301 | } 302 | 303 | } 304 | -------------------------------------------------------------------------------- /src/main/java/info/hb/lookup/object/proc/CannyEdgeDetector.java: -------------------------------------------------------------------------------- 1 | package info.hb.lookup.object.proc; 2 | 3 | import java.awt.image.BufferedImage; 4 | import java.util.Arrays; 5 | 6 | /** 7 | *

8 | * This software has been released into the public domain. 9 | * Please read the notes in this source file for additional information. 10 | * 11 | *

12 | * 13 | *

14 | * This class provides a configurable implementation of the Canny edge detection 15 | * algorithm. This classic algorithm has a number of shortcomings, but remains 16 | * an effective tool in many scenarios. This class is designed 17 | * for single threaded use only. 18 | *

19 | * 20 | *

21 | * Sample usage: 22 | *

23 | * 24 | *
 25 |  * 
 26 |  * //create the detector
 27 |  * CannyEdgeDetector detector = new CannyEdgeDetector();
 28 |  * //adjust its parameters as desired
 29 |  * detector.setLowThreshold(0.5f);
 30 |  * detector.setHighThreshold(1f);
 31 |  * //apply it to an image
 32 |  * detector.setSourceImage(frame);
 33 |  * detector.process();
 34 |  * BufferedImage edges = detector.getEdgesImage();
 35 |  * 
 36 |  * 
37 | * 38 | *

39 | * For a more complete understanding of this edge detector's parameters consult 40 | * an explanation of the algorithm. 41 | *

42 | * 43 | * @author Tom Gibara 44 | * 45 | * http://www.tomgibara.com/computer-vision/canny-edge-detector 46 | * 47 | */ 48 | 49 | public class CannyEdgeDetector { 50 | 51 | // statics 52 | private final static float GAUSSIAN_CUT_OFF = 0.005f; 53 | private final static float MAGNITUDE_SCALE = 100F; 54 | private final static float MAGNITUDE_LIMIT = 1000F; 55 | private final static int MAGNITUDE_MAX = (int) (MAGNITUDE_SCALE * MAGNITUDE_LIMIT); 56 | 57 | // fields 58 | private int height; 59 | private int width; 60 | private int picsize; 61 | private int[] data; 62 | private int[] magnitude; 63 | private BufferedImage sourceImage; 64 | private BufferedImage edgesImage; 65 | 66 | private float gaussianKernelRadius; 67 | private float lowThreshold; 68 | private float highThreshold; 69 | private int gaussianKernelWidth; 70 | private boolean contrastNormalized; 71 | 72 | private float[] xConv; 73 | private float[] yConv; 74 | private float[] xGradient; 75 | private float[] yGradient; 76 | 77 | // constructors 78 | 79 | /** 80 | * Constructs a new detector with default parameters. 81 | */ 82 | 83 | public CannyEdgeDetector() { 84 | lowThreshold = 2.5f; 85 | highThreshold = 7.5f; 86 | gaussianKernelRadius = 2f; 87 | gaussianKernelWidth = 16; 88 | contrastNormalized = false; 89 | } 90 | 91 | // accessors 92 | 93 | /** 94 | * The image that provides the luminance data used by this detector to 95 | * generate edges. 96 | * 97 | * @return the source image, or null 98 | */ 99 | 100 | public BufferedImage getSourceImage() { 101 | return sourceImage; 102 | } 103 | 104 | /** 105 | * Specifies the image that will provide the luminance data in which edges 106 | * will be detected. A source image must be set before the process method is 107 | * called. 108 | * 109 | * @param image 110 | * a source of luminance data 111 | */ 112 | 113 | public void setSourceImage(BufferedImage image) { 114 | sourceImage = image; 115 | } 116 | 117 | /** 118 | * Obtains an image containing the edges detected during the last call to 119 | * the process method. The buffered image is an opaque image of type 120 | * BufferedImage.TYPE_INT_ARGB in which edge pixels are white and all other 121 | * pixels are black. 122 | * 123 | * @return an image containing the detected edges, or null if the process 124 | * method has not yet been called. 125 | */ 126 | 127 | public BufferedImage getEdgesImage() { 128 | return edgesImage; 129 | } 130 | 131 | /** 132 | * Sets the edges image. Calling this method will not change the operation 133 | * of the edge detector in any way. It is intended to provide a means by 134 | * which the memory referenced by the detector object may be reduced. 135 | * 136 | * @param edgesImage 137 | * expected (though not required) to be null 138 | */ 139 | 140 | public void setEdgesImage(BufferedImage edgesImage) { 141 | this.edgesImage = edgesImage; 142 | } 143 | 144 | /** 145 | * The low threshold for hysteresis. The default value is 2.5. 146 | * 147 | * @return the low hysteresis threshold 148 | */ 149 | 150 | public float getLowThreshold() { 151 | return lowThreshold; 152 | } 153 | 154 | /** 155 | * Sets the low threshold for hysteresis. Suitable values for this parameter 156 | * must be determined experimentally for each application. It is nonsensical 157 | * (though not prohibited) for this value to exceed the high threshold 158 | * value. 159 | * 160 | * @param threshold 161 | * a low hysteresis threshold 162 | */ 163 | 164 | public void setLowThreshold(float threshold) { 165 | if (threshold < 0) 166 | throw new IllegalArgumentException(); 167 | lowThreshold = threshold; 168 | } 169 | 170 | /** 171 | * The high threshold for hysteresis. The default value is 7.5. 172 | * 173 | * @return the high hysteresis threshold 174 | */ 175 | 176 | public float getHighThreshold() { 177 | return highThreshold; 178 | } 179 | 180 | /** 181 | * Sets the high threshold for hysteresis. Suitable values for this 182 | * parameter must be determined experimentally for each application. It is 183 | * nonsensical (though not prohibited) for this value to be less than the 184 | * low threshold value. 185 | * 186 | * @param threshold 187 | * a high hysteresis threshold 188 | */ 189 | 190 | public void setHighThreshold(float threshold) { 191 | if (threshold < 0) 192 | throw new IllegalArgumentException(); 193 | highThreshold = threshold; 194 | } 195 | 196 | /** 197 | * The number of pixels across which the Gaussian kernel is applied. The 198 | * default value is 16. 199 | * 200 | * @return the radius of the convolution operation in pixels 201 | */ 202 | 203 | public int getGaussianKernelWidth() { 204 | return gaussianKernelWidth; 205 | } 206 | 207 | /** 208 | * The number of pixels across which the Gaussian kernel is applied. This 209 | * implementation will reduce the radius if the contribution of pixel values 210 | * is deemed negligable, so this is actually a maximum radius. 211 | * 212 | * @param gaussianKernelWidth 213 | * a radius for the convolution operation in pixels, at least 2. 214 | */ 215 | 216 | public void setGaussianKernelWidth(int gaussianKernelWidth) { 217 | if (gaussianKernelWidth < 2) 218 | throw new IllegalArgumentException(); 219 | this.gaussianKernelWidth = gaussianKernelWidth; 220 | } 221 | 222 | /** 223 | * The radius of the Gaussian convolution kernel used to smooth the source 224 | * image prior to gradient calculation. The default value is 16. 225 | * 226 | * @return the Gaussian kernel radius in pixels 227 | */ 228 | 229 | public float getGaussianKernelRadius() { 230 | return gaussianKernelRadius; 231 | } 232 | 233 | /** 234 | * Sets the radius of the Gaussian convolution kernel used to smooth the 235 | * source image prior to gradient calculation. 236 | * 237 | * @return a Gaussian kernel radius in pixels, must exceed 0.1f. 238 | */ 239 | 240 | public void setGaussianKernelRadius(float gaussianKernelRadius) { 241 | if (gaussianKernelRadius < 0.1f) 242 | throw new IllegalArgumentException(); 243 | this.gaussianKernelRadius = gaussianKernelRadius; 244 | } 245 | 246 | /** 247 | * Whether the luminance data extracted from the source image is normalized 248 | * by linearizing its histogram prior to edge extraction. The default value 249 | * is false. 250 | * 251 | * @return whether the contrast is normalized 252 | */ 253 | 254 | public boolean isContrastNormalized() { 255 | return contrastNormalized; 256 | } 257 | 258 | /** 259 | * Sets whether the contrast is normalized 260 | * 261 | * @param contrastNormalized 262 | * true if the contrast should be normalized, false otherwise 263 | */ 264 | 265 | public void setContrastNormalized(boolean contrastNormalized) { 266 | this.contrastNormalized = contrastNormalized; 267 | } 268 | 269 | // methods 270 | 271 | public void process() { 272 | width = sourceImage.getWidth(); 273 | height = sourceImage.getHeight(); 274 | picsize = width * height; 275 | initArrays(); 276 | readLuminance(); 277 | if (contrastNormalized) 278 | normalizeContrast(); 279 | computeGradients(gaussianKernelRadius, gaussianKernelWidth); 280 | int low = Math.round(lowThreshold * MAGNITUDE_SCALE); 281 | int high = Math.round(highThreshold * MAGNITUDE_SCALE); 282 | performHysteresis(low, high); 283 | thresholdEdges(); 284 | writeEdges(data); 285 | } 286 | 287 | // private utility methods 288 | 289 | private void initArrays() { 290 | if (data == null || picsize != data.length) { 291 | data = new int[picsize]; 292 | magnitude = new int[picsize]; 293 | 294 | xConv = new float[picsize]; 295 | yConv = new float[picsize]; 296 | xGradient = new float[picsize]; 297 | yGradient = new float[picsize]; 298 | } 299 | } 300 | 301 | // NOTE: The elements of the method below (specifically the technique for 302 | // non-maximal suppression and the technique for gradient computation) 303 | // are derived from an implementation posted in the following forum (with 304 | // the 305 | // clear intent of others using the code): 306 | // http://forum.java.sun.com/thread.jspa?threadID=546211&start=45&tstart=0 307 | // My code effectively mimics the algorithm exhibited above. 308 | // Since I don't know the providence of the code that was posted it is a 309 | // possibility (though I think a very remote one) that this code violates 310 | // someone's intellectual property rights. If this concerns you feel free to 311 | // contact me for an alternative, though less efficient, implementation. 312 | 313 | private void computeGradients(float kernelRadius, int kernelWidth) { 314 | 315 | // generate the gaussian convolution masks 316 | float kernel[] = new float[kernelWidth]; 317 | float diffKernel[] = new float[kernelWidth]; 318 | int kwidth; 319 | for (kwidth = 0; kwidth < kernelWidth; kwidth++) { 320 | float g1 = gaussian(kwidth, kernelRadius); 321 | if (g1 <= GAUSSIAN_CUT_OFF && kwidth >= 2) 322 | break; 323 | float g2 = gaussian(kwidth - 0.5f, kernelRadius); 324 | float g3 = gaussian(kwidth + 0.5f, kernelRadius); 325 | kernel[kwidth] = (g1 + g2 + g3) / 3f / (2f * (float) Math.PI * kernelRadius * kernelRadius); 326 | diffKernel[kwidth] = g3 - g2; 327 | } 328 | 329 | int initX = kwidth - 1; 330 | int maxX = width - (kwidth - 1); 331 | int initY = width * (kwidth - 1); 332 | int maxY = width * (height - (kwidth - 1)); 333 | 334 | // perform convolution in x and y directions 335 | for (int x = initX; x < maxX; x++) { 336 | for (int y = initY; y < maxY; y += width) { 337 | int index = x + y; 338 | float sumX = data[index] * kernel[0]; 339 | float sumY = sumX; 340 | int xOffset = 1; 341 | int yOffset = width; 342 | for (; xOffset < kwidth;) { 343 | sumY += kernel[xOffset] * (data[index - yOffset] + data[index + yOffset]); 344 | sumX += kernel[xOffset] * (data[index - xOffset] + data[index + xOffset]); 345 | yOffset += width; 346 | xOffset++; 347 | } 348 | 349 | yConv[index] = sumY; 350 | xConv[index] = sumX; 351 | } 352 | 353 | } 354 | 355 | for (int x = initX; x < maxX; x++) { 356 | for (int y = initY; y < maxY; y += width) { 357 | float sum = 0f; 358 | int index = x + y; 359 | for (int i = 1; i < kwidth; i++) 360 | sum += diffKernel[i] * (yConv[index - i] - yConv[index + i]); 361 | 362 | xGradient[index] = sum; 363 | } 364 | 365 | } 366 | 367 | for (int x = kwidth; x < width - kwidth; x++) { 368 | for (int y = initY; y < maxY; y += width) { 369 | float sum = 0.0f; 370 | int index = x + y; 371 | int yOffset = width; 372 | for (int i = 1; i < kwidth; i++) { 373 | sum += diffKernel[i] * (xConv[index - yOffset] - xConv[index + yOffset]); 374 | yOffset += width; 375 | } 376 | 377 | yGradient[index] = sum; 378 | } 379 | 380 | } 381 | 382 | initX = kwidth; 383 | maxX = width - kwidth; 384 | initY = width * kwidth; 385 | maxY = width * (height - kwidth); 386 | for (int x = initX; x < maxX; x++) { 387 | for (int y = initY; y < maxY; y += width) { 388 | int index = x + y; 389 | int indexN = index - width; 390 | int indexS = index + width; 391 | int indexW = index - 1; 392 | int indexE = index + 1; 393 | int indexNW = indexN - 1; 394 | int indexNE = indexN + 1; 395 | int indexSW = indexS - 1; 396 | int indexSE = indexS + 1; 397 | 398 | float xGrad = xGradient[index]; 399 | float yGrad = yGradient[index]; 400 | float gradMag = hypot(xGrad, yGrad); 401 | 402 | // perform non-maximal supression 403 | float nMag = hypot(xGradient[indexN], yGradient[indexN]); 404 | float sMag = hypot(xGradient[indexS], yGradient[indexS]); 405 | float wMag = hypot(xGradient[indexW], yGradient[indexW]); 406 | float eMag = hypot(xGradient[indexE], yGradient[indexE]); 407 | float neMag = hypot(xGradient[indexNE], yGradient[indexNE]); 408 | float seMag = hypot(xGradient[indexSE], yGradient[indexSE]); 409 | float swMag = hypot(xGradient[indexSW], yGradient[indexSW]); 410 | float nwMag = hypot(xGradient[indexNW], yGradient[indexNW]); 411 | float tmp; 412 | /* 413 | * An explanation of what's happening here, for those who want 414 | * to understand the source: This performs the "non-maximal 415 | * supression" phase of the Canny edge detection in which we 416 | * need to compare the gradient magnitude to that in the 417 | * direction of the gradient; only if the value is a local 418 | * maximum do we consider the point as an edge candidate. 419 | * 420 | * We need to break the comparison into a number of different 421 | * cases depending on the gradient direction so that the 422 | * appropriate values can be used. To avoid computing the 423 | * gradient direction, we use two simple comparisons: first we 424 | * check that the partial derivatives have the same sign (1) and 425 | * then we check which is larger (2). As a consequence, we have 426 | * reduced the problem to one of four identical cases that each 427 | * test the central gradient magnitude against the values at two 428 | * points with 'identical support'; what this means is that the 429 | * geometry required to accurately interpolate the magnitude of 430 | * gradient function at those points has an identical geometry 431 | * (upto right-angled-rotation/reflection). 432 | * 433 | * When comparing the central gradient to the two interpolated 434 | * values, we avoid performing any divisions by multiplying both 435 | * sides of each inequality by the greater of the two partial 436 | * derivatives. The common comparand is stored in a temporary 437 | * variable (3) and reused in the mirror case (4). 438 | */ 439 | if (xGrad * yGrad <= 0 /* (1) */ 440 | ? Math.abs(xGrad) >= Math.abs(yGrad) /* (2) */ 441 | ? (tmp = Math.abs(xGrad * gradMag)) >= Math.abs(yGrad * neMag - (xGrad + yGrad) * eMag) /* 442 | * ( 443 | * 3 444 | * ) 445 | */ 446 | && tmp > Math.abs(yGrad * swMag - (xGrad + yGrad) * wMag) /* 447 | * ( 448 | * 4 449 | * ) 450 | */ 451 | : (tmp = Math.abs(yGrad * gradMag)) >= Math.abs(xGrad * neMag - (yGrad + xGrad) * nMag) /* 452 | * ( 453 | * 3 454 | * ) 455 | */ 456 | && tmp > Math.abs(xGrad * swMag - (yGrad + xGrad) * sMag) /* 457 | * ( 458 | * 4 459 | * ) 460 | */ 461 | : Math.abs(xGrad) >= Math.abs(yGrad) /* (2) */ 462 | ? (tmp = Math.abs(xGrad * gradMag)) >= Math.abs(yGrad * seMag + (xGrad - yGrad) * eMag) /* 463 | * ( 464 | * 3 465 | * ) 466 | */ 467 | && tmp > Math.abs(yGrad * nwMag + (xGrad - yGrad) * wMag) /* 468 | * ( 469 | * 4 470 | * ) 471 | */ 472 | : (tmp = Math.abs(yGrad * gradMag)) >= Math.abs(xGrad * seMag + (yGrad - xGrad) * sMag) /* 473 | * ( 474 | * 3 475 | * ) 476 | */ 477 | && tmp > Math.abs(xGrad * nwMag + (yGrad - xGrad) * nMag) /* 478 | * ( 479 | * 4 480 | * ) 481 | */ 482 | ) { 483 | magnitude[index] = gradMag >= MAGNITUDE_LIMIT ? MAGNITUDE_MAX : (int) (MAGNITUDE_SCALE * gradMag); 484 | // NOTE: The orientation of the edge is not employed by this 485 | // implementation. It is a simple matter to compute it at 486 | // this point as: Math.atan2(yGrad, xGrad); 487 | } else { 488 | magnitude[index] = 0; 489 | } 490 | } 491 | } 492 | } 493 | 494 | // NOTE: It is quite feasible to replace the implementation of this method 495 | // with one which only loosely approximates the hypot function. I've tested 496 | // simple approximations such as Math.abs(x) + Math.abs(y) and they work 497 | // fine. 498 | private float hypot(float x, float y) { 499 | return (float) Math.hypot(x, y); 500 | } 501 | 502 | private float gaussian(float x, float sigma) { 503 | return (float) Math.exp(-(x * x) / (2f * sigma * sigma)); 504 | } 505 | 506 | private void performHysteresis(int low, int high) { 507 | // NOTE: this implementation reuses the data array to store both 508 | // luminance data from the image, and edge intensity from the 509 | // processing. 510 | // This is done for memory efficiency, other implementations may wish 511 | // to separate these functions. 512 | Arrays.fill(data, 0); 513 | 514 | int offset = 0; 515 | for (int y = 0; y < height; y++) { 516 | for (int x = 0; x < width; x++) { 517 | if (data[offset] == 0 && magnitude[offset] >= high) { 518 | follow(x, y, offset, low); 519 | } 520 | offset++; 521 | } 522 | } 523 | } 524 | 525 | private void follow(int x1, int y1, int i1, int threshold) { 526 | int x0 = x1 == 0 ? x1 : x1 - 1; 527 | int x2 = x1 == width - 1 ? x1 : x1 + 1; 528 | int y0 = y1 == 0 ? y1 : y1 - 1; 529 | int y2 = y1 == height - 1 ? y1 : y1 + 1; 530 | 531 | data[i1] = magnitude[i1]; 532 | for (int x = x0; x <= x2; x++) { 533 | for (int y = y0; y <= y2; y++) { 534 | int i2 = x + y * width; 535 | if ((y != y1 || x != x1) && data[i2] == 0 && magnitude[i2] >= threshold) { 536 | follow(x, y, i2, threshold); 537 | return; 538 | } 539 | } 540 | } 541 | } 542 | 543 | private void thresholdEdges() { 544 | for (int i = 0; i < picsize; i++) { 545 | data[i] = data[i] > 0 ? -1 : 0xff000000; 546 | } 547 | } 548 | 549 | private int luminance(float r, float g, float b) { 550 | return Math.round(0.299f * r + 0.587f * g + 0.114f * b); 551 | } 552 | 553 | private void readLuminance() { 554 | int type = sourceImage.getType(); 555 | if (type == BufferedImage.TYPE_INT_RGB || type == BufferedImage.TYPE_INT_ARGB) { 556 | int[] pixels = (int[]) sourceImage.getData().getDataElements(0, 0, width, height, null); 557 | for (int i = 0; i < picsize; i++) { 558 | int p = pixels[i]; 559 | int r = (p & 0xff0000) >> 16; 560 | int g = (p & 0xff00) >> 8; 561 | int b = p & 0xff; 562 | data[i] = luminance(r, g, b); 563 | } 564 | } else if (type == BufferedImage.TYPE_BYTE_GRAY) { 565 | byte[] pixels = (byte[]) sourceImage.getData().getDataElements(0, 0, width, height, null); 566 | for (int i = 0; i < picsize; i++) { 567 | data[i] = (pixels[i] & 0xff); 568 | } 569 | } else if (type == BufferedImage.TYPE_USHORT_GRAY) { 570 | short[] pixels = (short[]) sourceImage.getData().getDataElements(0, 0, width, height, null); 571 | for (int i = 0; i < picsize; i++) { 572 | data[i] = (pixels[i] & 0xffff) / 256; 573 | } 574 | } else if (type == BufferedImage.TYPE_3BYTE_BGR) { 575 | byte[] pixels = (byte[]) sourceImage.getData().getDataElements(0, 0, width, height, null); 576 | int offset = 0; 577 | for (int i = 0; i < picsize; i++) { 578 | int b = pixels[offset++] & 0xff; 579 | int g = pixels[offset++] & 0xff; 580 | int r = pixels[offset++] & 0xff; 581 | data[i] = luminance(r, g, b); 582 | } 583 | } else { 584 | for (int x = 0; x < sourceImage.getWidth(); x++) { 585 | for (int y = 0; y < sourceImage.getHeight(); y++) { 586 | int p = sourceImage.getRGB(x, y); 587 | int r = (p & 0xff0000) >> 16; 588 | int g = (p & 0xff00) >> 8; 589 | int b = p & 0xff; 590 | data[y * sourceImage.getWidth() + x] = luminance(r, g, b); 591 | } 592 | } 593 | // original exception. i prefer slow run over error 594 | // 595 | // throw new IllegalArgumentException("Unsupported image type: " + 596 | // type); 597 | } 598 | } 599 | 600 | private void normalizeContrast() { 601 | int[] histogram = new int[256]; 602 | for (int i = 0; i < data.length; i++) { 603 | histogram[data[i]]++; 604 | } 605 | int[] remap = new int[256]; 606 | int sum = 0; 607 | int j = 0; 608 | for (int i = 0; i < histogram.length; i++) { 609 | sum += histogram[i]; 610 | int target = sum * 255 / picsize; 611 | for (int k = j + 1; k <= target; k++) { 612 | remap[k] = i; 613 | } 614 | j = target; 615 | } 616 | 617 | for (int i = 0; i < data.length; i++) { 618 | data[i] = remap[data[i]]; 619 | } 620 | } 621 | 622 | private void writeEdges(int pixels[]) { 623 | // NOTE: There is currently no mechanism for obtaining the edge data 624 | // in any other format other than an INT_ARGB type BufferedImage. 625 | // This may be easily remedied by providing alternative accessors. 626 | if (edgesImage == null) { 627 | edgesImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); 628 | } 629 | edgesImage.getWritableTile(0, 0).setDataElements(0, 0, width, height, pixels); 630 | } 631 | 632 | } 633 | --------------------------------------------------------------------------------