├── .gitattributes ├── src ├── main │ ├── filtered │ │ └── com │ │ │ └── machinezoo │ │ │ └── sourceafis │ │ │ └── version.txt │ └── java │ │ ├── com │ │ └── machinezoo │ │ │ └── sourceafis │ │ │ ├── engine │ │ │ ├── features │ │ │ │ ├── MinutiaType.java │ │ │ │ ├── SkeletonType.java │ │ │ │ ├── FeatureMinutia.java │ │ │ │ ├── IndexedEdge.java │ │ │ │ ├── SearchMinutia.java │ │ │ │ ├── SkeletonMinutia.java │ │ │ │ ├── Skeleton.java │ │ │ │ ├── SkeletonRidge.java │ │ │ │ ├── EdgeShape.java │ │ │ │ └── NeighborEdge.java │ │ │ ├── images │ │ │ │ ├── package-info.java │ │ │ │ ├── DecodedImage.java │ │ │ │ ├── AndroidBitmapFactory.java │ │ │ │ ├── AndroidBitmap.java │ │ │ │ ├── AndroidImageDecoder.java │ │ │ │ ├── ImageIODecoder.java │ │ │ │ ├── WsqDecoder.java │ │ │ │ └── ImageDecoder.java │ │ │ ├── primitives │ │ │ │ ├── DpiConverter.java │ │ │ │ ├── Integers.java │ │ │ │ ├── IntRange.java │ │ │ │ ├── DoublePoint.java │ │ │ │ ├── Doubles.java │ │ │ │ ├── BlockGrid.java │ │ │ │ ├── IntMatrix.java │ │ │ │ ├── BlockMap.java │ │ │ │ ├── FloatAngle.java │ │ │ │ ├── DoubleMatrix.java │ │ │ │ ├── DoublePointMatrix.java │ │ │ │ ├── HistogramCube.java │ │ │ │ ├── BooleanMatrix.java │ │ │ │ ├── DoubleAngle.java │ │ │ │ ├── CircularArray.java │ │ │ │ ├── IntRect.java │ │ │ │ ├── MemoryEstimates.java │ │ │ │ └── IntPoint.java │ │ │ ├── package-info.java │ │ │ ├── templates │ │ │ │ ├── package-info.java │ │ │ │ ├── TemplateResolution.java │ │ │ │ ├── FeatureTemplate.java │ │ │ │ ├── TemplateCodec.java │ │ │ │ ├── SearchTemplate.java │ │ │ │ ├── PersistentTemplate.java │ │ │ │ ├── Ansi378v2009Codec.java │ │ │ │ ├── Ansi378v2009Am1Codec.java │ │ │ │ ├── Iso19794p2v2011Codec.java │ │ │ │ ├── Ansi378v2004Codec.java │ │ │ │ └── Iso19794p2v2005Codec.java │ │ │ ├── matcher │ │ │ │ ├── MinutiaPair.java │ │ │ │ ├── RootList.java │ │ │ │ ├── MinutiaPairPool.java │ │ │ │ ├── ScoringData.java │ │ │ │ ├── MatcherThread.java │ │ │ │ ├── Probe.java │ │ │ │ ├── RootEnumerator.java │ │ │ │ ├── PairingGraph.java │ │ │ │ ├── MatcherEngine.java │ │ │ │ ├── EdgeSpider.java │ │ │ │ └── EdgeHashes.java │ │ │ ├── transparency │ │ │ │ ├── ConsistentHashEntry.java │ │ │ │ ├── ConsistentSkeletonRidge.java │ │ │ │ ├── ConsistentEdgePair.java │ │ │ │ ├── NoTransparency.java │ │ │ │ ├── ConsistentMinutiaPair.java │ │ │ │ ├── ConsistentPairingGraph.java │ │ │ │ ├── ConsistentSkeleton.java │ │ │ │ ├── TransparencyZip.java │ │ │ │ └── TransparencyMimes.java │ │ │ ├── extractor │ │ │ │ ├── skeletons │ │ │ │ │ ├── SkeletonDotFilter.java │ │ │ │ │ ├── SkeletonGap.java │ │ │ │ │ ├── SkeletonFilters.java │ │ │ │ │ ├── Skeletons.java │ │ │ │ │ ├── SkeletonFragmentFilter.java │ │ │ │ │ ├── SkeletonTailFilter.java │ │ │ │ │ ├── SkeletonKnotFilter.java │ │ │ │ │ ├── SkeletonPoreFilter.java │ │ │ │ │ ├── SkeletonGapFilter.java │ │ │ │ │ └── BinaryThinning.java │ │ │ │ ├── minutiae │ │ │ │ │ ├── InnerMinutiaeFilter.java │ │ │ │ │ ├── MinutiaCloudFilter.java │ │ │ │ │ ├── MinutiaCollector.java │ │ │ │ │ └── TopMinutiaeFilter.java │ │ │ │ ├── AbsoluteContrastMask.java │ │ │ │ ├── ClippedContrast.java │ │ │ │ ├── RelativeContrastMask.java │ │ │ │ ├── ImageResizer.java │ │ │ │ ├── LocalHistograms.java │ │ │ │ ├── VoteFilter.java │ │ │ │ ├── BlockOrientations.java │ │ │ │ ├── BinarizedImage.java │ │ │ │ ├── FeatureExtractor.java │ │ │ │ ├── SegmentationMask.java │ │ │ │ ├── OrientedSmoothing.java │ │ │ │ ├── ImageEqualization.java │ │ │ │ └── PixelwiseOrientations.java │ │ │ └── configuration │ │ │ │ ├── PlatformCheck.java │ │ │ │ └── Parameters.java │ │ │ ├── package-info.java │ │ │ └── FingerprintImageOptions.java │ │ └── module-info.java └── test │ ├── resources │ └── com │ │ └── machinezoo │ │ └── sourceafis │ │ ├── probe.bmp │ │ ├── probe.jpeg │ │ ├── probe.png │ │ ├── matching.png │ │ ├── gray-probe.dat │ │ ├── iso-probe.dat │ │ ├── nonmatching.png │ │ ├── gray-matching.dat │ │ ├── iso-matching.dat │ │ ├── wsq-converted.png │ │ ├── wsq-original.wsq │ │ ├── gray-nonmatching.dat │ │ └── iso-nonmatching.dat │ └── java │ └── com │ └── machinezoo │ └── sourceafis │ ├── engine │ └── primitives │ │ ├── IntRangeTest.java │ │ ├── IntegersTest.java │ │ ├── DoublePointTest.java │ │ ├── BlockGridTest.java │ │ ├── BlockMapTest.java │ │ ├── DoublesTest.java │ │ ├── DoubleMatrixTest.java │ │ ├── HistogramCubeTest.java │ │ ├── DoublePointMatrixTest.java │ │ ├── IntRectTest.java │ │ └── BooleanMatrixTest.java │ ├── FingerprintMatcherTest.java │ ├── TestResources.java │ ├── FingerprintTransparencyTest.java │ ├── FingerprintImageTest.java │ └── FingerprintCompatibilityTest.java ├── COPYRIGHT ├── .pydevproject ├── .gitignore ├── .github └── workflows │ ├── build.yml │ └── release.yml ├── CONTRIBUTING.md └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | *.dat binary 2 | 3 | -------------------------------------------------------------------------------- /src/main/filtered/com/machinezoo/sourceafis/version.txt: -------------------------------------------------------------------------------- 1 | ${project.version} 2 | -------------------------------------------------------------------------------- /src/test/resources/com/machinezoo/sourceafis/probe.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertvazan/sourceafis-java/HEAD/src/test/resources/com/machinezoo/sourceafis/probe.bmp -------------------------------------------------------------------------------- /src/test/resources/com/machinezoo/sourceafis/probe.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertvazan/sourceafis-java/HEAD/src/test/resources/com/machinezoo/sourceafis/probe.jpeg -------------------------------------------------------------------------------- /src/test/resources/com/machinezoo/sourceafis/probe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertvazan/sourceafis-java/HEAD/src/test/resources/com/machinezoo/sourceafis/probe.png -------------------------------------------------------------------------------- /src/test/resources/com/machinezoo/sourceafis/matching.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertvazan/sourceafis-java/HEAD/src/test/resources/com/machinezoo/sourceafis/matching.png -------------------------------------------------------------------------------- /src/test/resources/com/machinezoo/sourceafis/gray-probe.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertvazan/sourceafis-java/HEAD/src/test/resources/com/machinezoo/sourceafis/gray-probe.dat -------------------------------------------------------------------------------- /src/test/resources/com/machinezoo/sourceafis/iso-probe.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertvazan/sourceafis-java/HEAD/src/test/resources/com/machinezoo/sourceafis/iso-probe.dat -------------------------------------------------------------------------------- /src/test/resources/com/machinezoo/sourceafis/nonmatching.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertvazan/sourceafis-java/HEAD/src/test/resources/com/machinezoo/sourceafis/nonmatching.png -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | Robert Važan's SourceAFIS for Java 2 | https://sourceafis.machinezoo.com/java 3 | Copyright 2009-2025 Robert Važan and contributors 4 | Distributed under Apache License 2.0. 5 | -------------------------------------------------------------------------------- /src/test/resources/com/machinezoo/sourceafis/gray-matching.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertvazan/sourceafis-java/HEAD/src/test/resources/com/machinezoo/sourceafis/gray-matching.dat -------------------------------------------------------------------------------- /src/test/resources/com/machinezoo/sourceafis/iso-matching.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertvazan/sourceafis-java/HEAD/src/test/resources/com/machinezoo/sourceafis/iso-matching.dat -------------------------------------------------------------------------------- /src/test/resources/com/machinezoo/sourceafis/wsq-converted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertvazan/sourceafis-java/HEAD/src/test/resources/com/machinezoo/sourceafis/wsq-converted.png -------------------------------------------------------------------------------- /src/test/resources/com/machinezoo/sourceafis/wsq-original.wsq: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertvazan/sourceafis-java/HEAD/src/test/resources/com/machinezoo/sourceafis/wsq-original.wsq -------------------------------------------------------------------------------- /src/test/resources/com/machinezoo/sourceafis/gray-nonmatching.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertvazan/sourceafis-java/HEAD/src/test/resources/com/machinezoo/sourceafis/gray-nonmatching.dat -------------------------------------------------------------------------------- /src/test/resources/com/machinezoo/sourceafis/iso-nonmatching.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertvazan/sourceafis-java/HEAD/src/test/resources/com/machinezoo/sourceafis/iso-nonmatching.dat -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/features/MinutiaType.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.features; 3 | 4 | public enum MinutiaType { 5 | ENDING, 6 | BIFURCATION 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/images/package-info.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | @com.machinezoo.stagean.DraftCode("This whole package should be replaced by a library with sufficiently wide image decoder support.") 3 | package com.machinezoo.sourceafis.engine.images; 4 | -------------------------------------------------------------------------------- /.pydevproject: -------------------------------------------------------------------------------- 1 | 2 | 3 | Default 4 | python interpreter 5 | 6 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/primitives/DpiConverter.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.primitives; 3 | 4 | public class DpiConverter { 5 | public static int decode(int value, double dpi) { 6 | return (int)Math.round(value / dpi * 500); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/package-info.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | /* 3 | * Keep everything in engine subpackage to avoid conflicts with other modules, 4 | * which usually have root package that is a subpackage of com.machinezoo.sourceafis. 5 | */ 6 | package com.machinezoo.sourceafis.engine; 7 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/templates/package-info.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | @com.machinezoo.stagean.DraftCode("FingerprintIO could provide universal minutia template model, so that we don't have to handle every format separately here.") 3 | package com.machinezoo.sourceafis.engine.templates; 4 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/features/SkeletonType.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.features; 3 | 4 | public enum SkeletonType { 5 | RIDGES("ridges-"), VALLEYS("valleys-"); 6 | public final String prefix; 7 | SkeletonType(String prefix) { 8 | this.prefix = prefix; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/primitives/Integers.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.primitives; 3 | 4 | public class Integers { 5 | public static int sq(int value) { 6 | return value * value; 7 | } 8 | public static int roundUpDiv(int dividend, int divisor) { 9 | return (dividend + divisor - 1) / divisor; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/templates/TemplateResolution.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.templates; 3 | 4 | import com.machinezoo.sourceafis.engine.primitives.*; 5 | 6 | class TemplateResolution { 7 | double dpiX; 8 | double dpiY; 9 | IntPoint decode(int x, int y) { 10 | return new IntPoint(DpiConverter.decode(x, dpiX), DpiConverter.decode(y, dpiY)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/primitives/IntRange.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.primitives; 3 | 4 | public class IntRange { 5 | public static final IntRange ZERO = new IntRange(0, 0); 6 | public final int start; 7 | public final int end; 8 | public IntRange(int start, int end) { 9 | this.start = start; 10 | this.end = end; 11 | } 12 | public int length() { 13 | return end - start; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/matcher/MinutiaPair.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.matcher; 3 | 4 | public class MinutiaPair { 5 | public int probe; 6 | public int candidate; 7 | public int probeRef; 8 | public int candidateRef; 9 | public int distance; 10 | public int supportingEdges; 11 | @Override 12 | public String toString() { 13 | return String.format("%d<->%d @ %d<->%d #%d", probe, candidate, probeRef, candidateRef, supportingEdges); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Common operating system files 2 | *~ 3 | .DS_Store 4 | Thumbs.db 5 | .directory 6 | .fuse_hidden* 7 | .nfs* 8 | 9 | # Common IDE/editor files 10 | .vscode/ 11 | .idea/ 12 | 13 | # Maven build directory 14 | target/ 15 | 16 | # Maven-specific files 17 | pom.xml.tag 18 | pom.xml.releaseBackup 19 | pom.xml.versionsBackup 20 | pom.xml.next 21 | release.properties 22 | dependency-reduced-pom.xml 23 | buildNumber.properties 24 | .mvn/timing.properties 25 | .mvn/wrapper/maven-wrapper.jar 26 | 27 | # Compiled class files 28 | *.class 29 | 30 | # Log files 31 | *.log 32 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/transparency/ConsistentHashEntry.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.transparency; 3 | 4 | import java.util.*; 5 | import com.machinezoo.sourceafis.engine.features.*; 6 | 7 | @SuppressWarnings("unused") 8 | public class ConsistentHashEntry { 9 | public final int key; 10 | public final List edges; 11 | public ConsistentHashEntry(int key, List edges) { 12 | this.key = key; 13 | this.edges = edges; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/features/FeatureMinutia.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.features; 3 | 4 | import com.machinezoo.sourceafis.engine.primitives.*; 5 | 6 | public class FeatureMinutia { 7 | public final IntPoint position; 8 | public final float direction; 9 | public final MinutiaType type; 10 | public FeatureMinutia(IntPoint position, float direction, MinutiaType type) { 11 | this.position = position; 12 | this.direction = direction; 13 | this.type = type; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/test/java/com/machinezoo/sourceafis/engine/primitives/IntRangeTest.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.primitives; 3 | 4 | import static org.junit.jupiter.api.Assertions.*; 5 | import org.junit.jupiter.api.*; 6 | 7 | public class IntRangeTest { 8 | @Test 9 | public void constructor() { 10 | IntRange r = new IntRange(3, 10); 11 | assertEquals(3, r.start); 12 | assertEquals(10, r.end); 13 | } 14 | @Test 15 | public void length() { 16 | assertEquals(7, new IntRange(3, 10).length()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/templates/FeatureTemplate.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.templates; 3 | 4 | import java.util.*; 5 | import com.machinezoo.sourceafis.engine.features.*; 6 | import com.machinezoo.sourceafis.engine.primitives.*; 7 | 8 | public class FeatureTemplate { 9 | public final IntPoint size; 10 | public final List minutiae; 11 | public FeatureTemplate(IntPoint size, List minutiae) { 12 | this.size = size; 13 | this.minutiae = minutiae; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/transparency/ConsistentSkeletonRidge.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.transparency; 3 | 4 | import java.util.*; 5 | import com.machinezoo.sourceafis.engine.primitives.*; 6 | 7 | public class ConsistentSkeletonRidge { 8 | public final int start; 9 | public final int end; 10 | public final List points; 11 | public ConsistentSkeletonRidge(int start, int end, List points) { 12 | this.start = start; 13 | this.end = end; 14 | this.points = points; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/transparency/ConsistentEdgePair.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.transparency; 3 | 4 | import com.machinezoo.sourceafis.engine.matcher.*; 5 | 6 | public class ConsistentEdgePair { 7 | public final int probeFrom; 8 | public final int probeTo; 9 | public final int candidateFrom; 10 | public final int candidateTo; 11 | public ConsistentEdgePair(MinutiaPair pair) { 12 | probeFrom = pair.probeRef; 13 | probeTo = pair.probe; 14 | candidateFrom = pair.candidateRef; 15 | candidateTo = pair.candidate; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/extractor/skeletons/SkeletonDotFilter.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.extractor.skeletons; 3 | 4 | import java.util.*; 5 | import com.machinezoo.sourceafis.engine.features.*; 6 | 7 | public class SkeletonDotFilter { 8 | public static void apply(Skeleton skeleton) { 9 | List removed = new ArrayList<>(); 10 | for (SkeletonMinutia minutia : skeleton.minutiae) 11 | if (minutia.ridges.isEmpty()) 12 | removed.add(minutia); 13 | for (SkeletonMinutia minutia : removed) 14 | skeleton.removeMinutia(minutia); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/images/DecodedImage.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.images; 3 | 4 | public class DecodedImage { 5 | public int width; 6 | public int height; 7 | /* 8 | * Format identical to BufferedImage.TYPE_INT_ARGB, i.e. 8 bits for alpha (FF is opaque, 00 is transparent) 9 | * followed by 8-bit values for red, green, and blue in this order from highest bits to lowest. 10 | */ 11 | public int[] pixels; 12 | public DecodedImage(int width, int height, int[] pixels) { 13 | this.width = width; 14 | this.height = height; 15 | this.pixels = pixels; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/transparency/NoTransparency.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.transparency; 3 | 4 | import com.machinezoo.sourceafis.*; 5 | 6 | /* 7 | * To avoid null checks everywhere, we have one noop transparency logger as a fallback. 8 | */ 9 | public class NoTransparency extends FingerprintTransparency { 10 | public static final TransparencySink SINK; 11 | static { 12 | try (var transparency = new NoTransparency()) { 13 | SINK = TransparencySink.current(); 14 | } 15 | } 16 | @Override 17 | public boolean accepts(String key) { 18 | return false; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/images/AndroidBitmapFactory.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.images; 3 | 4 | import java.lang.reflect.*; 5 | import com.machinezoo.noexception.*; 6 | 7 | class AndroidBitmapFactory { 8 | static Class clazz = Exceptions.sneak().get(() -> Class.forName("android.graphics.BitmapFactory")); 9 | static Method decodeByteArray = Exceptions.sneak().get(() -> clazz.getMethod("decodeByteArray", byte[].class, int.class, int.class)); 10 | static AndroidBitmap decodeByteArray(byte[] data, int offset, int length) { 11 | return new AndroidBitmap(Exceptions.sneak().get(() -> decodeByteArray.invoke(null, data, offset, length))); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/package-info.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | /** 3 | * This package contains classes implementing SourceAFIS fingerprint recognition algorithm in Java. 4 | * Fingerprint images are processed into {@link com.machinezoo.sourceafis.FingerprintTemplate} objects. 5 | * Probe template is then converted to {@link com.machinezoo.sourceafis.FingerprintMatcher} 6 | * and one or more candidate templates are fed to its {@link com.machinezoo.sourceafis.FingerprintMatcher#match(FingerprintTemplate)} method 7 | * to obtain similarity scores. 8 | * 9 | * @see SourceAFIS for Java tutorial 10 | */ 11 | package com.machinezoo.sourceafis; 12 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/extractor/minutiae/InnerMinutiaeFilter.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.extractor.minutiae; 3 | 4 | import java.util.*; 5 | import com.machinezoo.sourceafis.engine.configuration.*; 6 | import com.machinezoo.sourceafis.engine.features.*; 7 | import com.machinezoo.sourceafis.engine.primitives.*; 8 | 9 | public class InnerMinutiaeFilter { 10 | public static void apply(List minutiae, BooleanMatrix mask) { 11 | minutiae.removeIf(minutia -> { 12 | IntPoint arrow = DoubleAngle.toVector(minutia.direction).multiply(-Parameters.MASK_DISPLACEMENT).round(); 13 | return !mask.get(minutia.position.plus(arrow), false); 14 | }); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/extractor/skeletons/SkeletonGap.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.extractor.skeletons; 3 | 4 | import com.machinezoo.sourceafis.engine.features.*; 5 | 6 | class SkeletonGap implements Comparable { 7 | int distance; 8 | SkeletonMinutia end1; 9 | SkeletonMinutia end2; 10 | @Override 11 | public int compareTo(SkeletonGap other) { 12 | int distanceCmp = Integer.compare(distance, other.distance); 13 | if (distanceCmp != 0) 14 | return distanceCmp; 15 | int end1Cmp = end1.position.compareTo(other.end1.position); 16 | if (end1Cmp != 0) 17 | return end1Cmp; 18 | return end2.position.compareTo(other.end2.position); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/extractor/skeletons/SkeletonFilters.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.extractor.skeletons; 3 | 4 | import com.machinezoo.sourceafis.engine.features.*; 5 | import com.machinezoo.sourceafis.engine.transparency.*; 6 | 7 | public class SkeletonFilters { 8 | public static void apply(Skeleton skeleton) { 9 | SkeletonDotFilter.apply(skeleton); 10 | // https://sourceafis.machinezoo.com/transparency/removed-dots 11 | TransparencySink.current().logSkeleton("removed-dots", skeleton); 12 | SkeletonPoreFilter.apply(skeleton); 13 | SkeletonGapFilter.apply(skeleton); 14 | SkeletonTailFilter.apply(skeleton); 15 | SkeletonFragmentFilter.apply(skeleton); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/transparency/ConsistentMinutiaPair.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.transparency; 3 | 4 | import static java.util.stream.Collectors.*; 5 | import java.util.*; 6 | import com.machinezoo.sourceafis.engine.matcher.*; 7 | 8 | public class ConsistentMinutiaPair { 9 | public final int probe; 10 | public final int candidate; 11 | public ConsistentMinutiaPair(int probe, int candidate) { 12 | this.probe = probe; 13 | this.candidate = candidate; 14 | } 15 | public static List roots(int count, MinutiaPair[] roots) { 16 | return Arrays.stream(roots).limit(count).map(p -> new ConsistentMinutiaPair(p.probe, p.candidate)).collect(toList()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/extractor/skeletons/Skeletons.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.extractor.skeletons; 3 | 4 | import com.machinezoo.sourceafis.engine.features.*; 5 | import com.machinezoo.sourceafis.engine.primitives.*; 6 | import com.machinezoo.sourceafis.engine.transparency.*; 7 | 8 | public class Skeletons { 9 | public static Skeleton create(BooleanMatrix binary, SkeletonType type) { 10 | // https://sourceafis.machinezoo.com/transparency/binarized-skeleton 11 | TransparencySink.current().log(type.prefix + "binarized-skeleton", binary); 12 | var thinned = BinaryThinning.thin(binary, type); 13 | var skeleton = SkeletonTracing.trace(thinned, type); 14 | SkeletonFilters.apply(skeleton); 15 | return skeleton; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/primitives/DoublePoint.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.primitives; 3 | 4 | public class DoublePoint { 5 | public static final DoublePoint ZERO = new DoublePoint(0, 0); 6 | public final double x; 7 | public final double y; 8 | public DoublePoint(double x, double y) { 9 | this.x = x; 10 | this.y = y; 11 | } 12 | public DoublePoint add(DoublePoint other) { 13 | return new DoublePoint(x + other.x, y + other.y); 14 | } 15 | public DoublePoint negate() { 16 | return new DoublePoint(-x, -y); 17 | } 18 | public DoublePoint multiply(double factor) { 19 | return new DoublePoint(factor * x, factor * y); 20 | } 21 | public IntPoint round() { 22 | return new IntPoint((int)Math.round(x), (int)Math.round(y)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/extractor/AbsoluteContrastMask.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.extractor; 3 | 4 | import com.machinezoo.sourceafis.engine.configuration.*; 5 | import com.machinezoo.sourceafis.engine.primitives.*; 6 | import com.machinezoo.sourceafis.engine.transparency.*; 7 | 8 | public class AbsoluteContrastMask { 9 | public static BooleanMatrix compute(DoubleMatrix contrast) { 10 | BooleanMatrix result = new BooleanMatrix(contrast.size()); 11 | for (IntPoint block : contrast.size()) 12 | if (contrast.get(block) < Parameters.MIN_ABSOLUTE_CONTRAST) 13 | result.set(block, true); 14 | // https://sourceafis.machinezoo.com/transparency/absolute-contrast-mask 15 | TransparencySink.current().log("absolute-contrast-mask", result); 16 | return result; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/matcher/RootList.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.matcher; 3 | 4 | import com.machinezoo.sourceafis.engine.configuration.*; 5 | import it.unimi.dsi.fastutil.ints.*; 6 | 7 | public class RootList { 8 | public final MinutiaPairPool pool; 9 | public int count; 10 | public final MinutiaPair[] pairs = new MinutiaPair[Parameters.MAX_TRIED_ROOTS]; 11 | public final IntSet duplicates = new IntOpenHashSet(); 12 | public RootList(MinutiaPairPool pool) { 13 | this.pool = pool; 14 | } 15 | public void add(MinutiaPair pair) { 16 | pairs[count] = pair; 17 | ++count; 18 | } 19 | public void discard() { 20 | for (int i = 0; i < count; ++i) { 21 | pool.release(pairs[i]); 22 | pairs[i] = null; 23 | } 24 | count = 0; 25 | duplicates.clear(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/matcher/MinutiaPairPool.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.matcher; 3 | 4 | import java.util.*; 5 | 6 | public class MinutiaPairPool { 7 | private MinutiaPair[] pool = new MinutiaPair[1]; 8 | private int pooled; 9 | public MinutiaPair allocate() { 10 | if (pooled > 0) { 11 | --pooled; 12 | MinutiaPair pair = pool[pooled]; 13 | pool[pooled] = null; 14 | return pair; 15 | } else 16 | return new MinutiaPair(); 17 | } 18 | public void release(MinutiaPair pair) { 19 | if (pooled >= pool.length) 20 | pool = Arrays.copyOf(pool, 2 * pool.length); 21 | pair.probe = 0; 22 | pair.candidate = 0; 23 | pair.probeRef = 0; 24 | pair.candidateRef = 0; 25 | pair.distance = 0; 26 | pair.supportingEdges = 0; 27 | pool[pooled] = pair; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/primitives/Doubles.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.primitives; 3 | 4 | public class Doubles { 5 | public static double sq(double value) { 6 | return value * value; 7 | } 8 | public static double interpolate(double start, double end, double position) { 9 | return start + position * (end - start); 10 | } 11 | public static double interpolate(double bottomleft, double bottomright, double topleft, double topright, double x, double y) { 12 | double left = interpolate(topleft, bottomleft, y); 13 | double right = interpolate(topright, bottomright, y); 14 | return interpolate(left, right, x); 15 | } 16 | public static double interpolateExponential(double start, double end, double position) { 17 | return Math.pow(end / start, position) * start; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/features/IndexedEdge.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.features; 3 | 4 | import com.machinezoo.sourceafis.engine.primitives.*; 5 | 6 | public class IndexedEdge extends EdgeShape { 7 | private final byte reference; 8 | private final byte neighbor; 9 | public IndexedEdge(SearchMinutia[] minutiae, int reference, int neighbor) { 10 | super(minutiae[reference], minutiae[neighbor]); 11 | this.reference = (byte)reference; 12 | this.neighbor = (byte)neighbor; 13 | } 14 | public int reference() { return Byte.toUnsignedInt(reference); } 15 | public int neighbor() { return Byte.toUnsignedInt(neighbor); } 16 | public static int memory() { return MemoryEstimates.object(Short.BYTES + 2 * Float.BYTES + 2 * Byte.BYTES, Float.BYTES); } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/transparency/ConsistentPairingGraph.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.transparency; 3 | 4 | import static java.util.stream.Collectors.*; 5 | import java.util.*; 6 | import com.machinezoo.sourceafis.engine.matcher.*; 7 | 8 | public class ConsistentPairingGraph { 9 | public final ConsistentMinutiaPair root; 10 | public final List tree; 11 | public final List support; 12 | public ConsistentPairingGraph(PairingGraph pairing) { 13 | root = new ConsistentMinutiaPair(pairing.tree[0].probe, pairing.tree[0].candidate); 14 | tree = Arrays.stream(pairing.tree).limit(pairing.count).skip(1).map(ConsistentEdgePair::new).collect(toList()); 15 | this.support = pairing.support.stream().map(ConsistentEdgePair::new).collect(toList()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/extractor/minutiae/MinutiaCloudFilter.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.extractor.minutiae; 3 | 4 | import static java.util.stream.Collectors.*; 5 | import java.util.*; 6 | import com.machinezoo.sourceafis.engine.configuration.*; 7 | import com.machinezoo.sourceafis.engine.features.*; 8 | import com.machinezoo.sourceafis.engine.primitives.*; 9 | 10 | public class MinutiaCloudFilter { 11 | public static void apply(List minutiae) { 12 | int radiusSq = Integers.sq(Parameters.MINUTIA_CLOUD_RADIUS); 13 | minutiae.removeAll(minutiae.stream() 14 | .filter(minutia -> Parameters.MAX_CLOUD_SIZE < minutiae.stream() 15 | .filter(neighbor -> neighbor.position.minus(minutia.position).lengthSq() <= radiusSq) 16 | .count() - 1) 17 | .collect(toList())); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/extractor/minutiae/MinutiaCollector.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.extractor.minutiae; 3 | 4 | import java.util.*; 5 | import com.machinezoo.sourceafis.engine.features.*; 6 | 7 | public class MinutiaCollector { 8 | public static void collect(List minutiae, Skeleton skeleton, MinutiaType type) { 9 | for (SkeletonMinutia sminutia : skeleton.minutiae) 10 | if (sminutia.ridges.size() == 1) 11 | minutiae.add(new FeatureMinutia(sminutia.position, sminutia.ridges.get(0).direction(), type)); 12 | } 13 | public static List collect(Skeleton ridges, Skeleton valleys) { 14 | var minutiae = new ArrayList(); 15 | collect(minutiae, ridges, MinutiaType.ENDING); 16 | collect(minutiae, valleys, MinutiaType.BIFURCATION); 17 | return minutiae; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/features/SearchMinutia.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.features; 3 | 4 | import com.machinezoo.sourceafis.engine.primitives.*; 5 | 6 | public class SearchMinutia { 7 | public final short x; 8 | public final short y; 9 | public final float direction; 10 | public final MinutiaType type; 11 | public SearchMinutia(FeatureMinutia feature) { 12 | this.x = (short)feature.position.x; 13 | this.y = (short)feature.position.y; 14 | this.direction = feature.direction; 15 | this.type = feature.type; 16 | } 17 | public FeatureMinutia feature() { return new FeatureMinutia(new IntPoint(x, y), direction, type); } 18 | public static int memory() { return MemoryEstimates.object(2 * Short.BYTES + Float.BYTES + MemoryEstimates.REFERENCE, MemoryEstimates.REFERENCE); } 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | push: 4 | branches: [ master ] 5 | pull_request: 6 | branches: [ master ] 7 | workflow_dispatch: 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-java@v4 14 | with: 15 | distribution: 'temurin' 16 | java-version: '11' 17 | cache: 'maven' 18 | - name: Build and test 19 | # -B runs in non-interactive (batch) mode 20 | # -V displays version information 21 | # jacoco:report generates test coverage report 22 | # -Dmaven.javadoc.failOnWarnings=true enforces Javadoc quality 23 | # -Dgpg.skip=true skips GPG signing, which is only done on release 24 | run: mvn -B -V install jacoco:report -Dmaven.javadoc.failOnWarnings=true -Dgpg.skip=true 25 | - name: Upload coverage to Codecov 26 | uses: codecov/codecov-action@v4 27 | -------------------------------------------------------------------------------- /src/test/java/com/machinezoo/sourceafis/engine/primitives/IntegersTest.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.primitives; 3 | 4 | import static org.junit.jupiter.api.Assertions.*; 5 | import org.junit.jupiter.api.*; 6 | 7 | public class IntegersTest { 8 | @Test 9 | public void sq() { 10 | assertEquals(9, Integers.sq(3)); 11 | assertEquals(9, Integers.sq(-3)); 12 | } 13 | @Test 14 | public void roundUpDiv() { 15 | assertEquals(3, Integers.roundUpDiv(9, 3)); 16 | assertEquals(3, Integers.roundUpDiv(8, 3)); 17 | assertEquals(3, Integers.roundUpDiv(7, 3)); 18 | assertEquals(2, Integers.roundUpDiv(6, 3)); 19 | assertEquals(5, Integers.roundUpDiv(20, 4)); 20 | assertEquals(5, Integers.roundUpDiv(19, 4)); 21 | assertEquals(5, Integers.roundUpDiv(18, 4)); 22 | assertEquals(5, Integers.roundUpDiv(17, 4)); 23 | assertEquals(4, Integers.roundUpDiv(16, 4)); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | workflow_dispatch: 4 | jobs: 5 | release: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v4 9 | - uses: actions/setup-java@v4 10 | with: 11 | distribution: 'temurin' 12 | java-version: '11' 13 | server-id: 'central' 14 | server-username: MAVEN_SERVER_USERNAME 15 | server-password: MAVEN_SERVER_PASSWORD 16 | gpg-private-key: ${{ secrets.MAVEN_SIGNING_KEY }} 17 | gpg-passphrase: MAVEN_SIGNING_PASSWORD 18 | cache: 'maven' 19 | - name: Publish to Maven Central 20 | # -B runs in non-interactive (batch) mode 21 | # -V displays version information 22 | run: mvn -B -V deploy 23 | env: 24 | MAVEN_SERVER_USERNAME: robertvazan 25 | MAVEN_SERVER_PASSWORD: ${{ secrets.MAVEN_SERVER_PASSWORD }} 26 | MAVEN_SIGNING_PASSWORD: ${{ secrets.MAVEN_SIGNING_PASSWORD }} 27 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/extractor/skeletons/SkeletonFragmentFilter.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.extractor.skeletons; 3 | 4 | import com.machinezoo.sourceafis.engine.configuration.*; 5 | import com.machinezoo.sourceafis.engine.features.*; 6 | import com.machinezoo.sourceafis.engine.transparency.*; 7 | 8 | public class SkeletonFragmentFilter { 9 | public static void apply(Skeleton skeleton) { 10 | for (SkeletonMinutia minutia : skeleton.minutiae) 11 | if (minutia.ridges.size() == 1) { 12 | SkeletonRidge ridge = minutia.ridges.get(0); 13 | if (ridge.end().ridges.size() == 1 && ridge.points.size() < Parameters.MIN_FRAGMENT_LENGTH) 14 | ridge.detach(); 15 | } 16 | SkeletonDotFilter.apply(skeleton); 17 | // https://sourceafis.machinezoo.com/transparency/removed-fragments 18 | TransparencySink.current().logSkeleton("removed-fragments", skeleton); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/matcher/ScoringData.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.matcher; 3 | 4 | public class ScoringData { 5 | public int minutiaCount; 6 | public double minutiaScore; 7 | public double minutiaFractionInProbe; 8 | public double minutiaFractionInCandidate; 9 | public double minutiaFraction; 10 | public double minutiaFractionScore; 11 | public int supportingEdgeSum; 12 | public int edgeCount; 13 | public double edgeScore; 14 | public int supportedMinutiaCount; 15 | public double supportedMinutiaScore; 16 | public int minutiaTypeHits; 17 | public double minutiaTypeScore; 18 | public int distanceErrorSum; 19 | public int distanceAccuracySum; 20 | public double distanceAccuracyScore; 21 | public float angleErrorSum; 22 | public float angleAccuracySum; 23 | public double angleAccuracyScore; 24 | public double totalScore; 25 | public double shapedScore; 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/templates/TemplateCodec.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.templates; 3 | 4 | import java.util.*; 5 | import com.machinezoo.fingerprintio.*; 6 | import com.machinezoo.noexception.*; 7 | 8 | public abstract class TemplateCodec { 9 | public abstract byte[] encode(List templates); 10 | public abstract List decode(byte[] serialized, ExceptionHandler handler); 11 | public static final Map ALL = new HashMap<>(); 12 | static { 13 | ALL.put(TemplateFormat.ANSI_378_2004, new Ansi378v2004Codec()); 14 | ALL.put(TemplateFormat.ANSI_378_2009, new Ansi378v2009Codec()); 15 | ALL.put(TemplateFormat.ANSI_378_2009_AM1, new Ansi378v2009Am1Codec()); 16 | ALL.put(TemplateFormat.ISO_19794_2_2005, new Iso19794p2v2005Codec()); 17 | ALL.put(TemplateFormat.ISO_19794_2_2011, new Iso19794p2v2011Codec()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/features/SkeletonMinutia.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.features; 3 | 4 | import java.util.*; 5 | import com.machinezoo.sourceafis.engine.primitives.*; 6 | 7 | public class SkeletonMinutia { 8 | public final IntPoint position; 9 | public final List ridges = new ArrayList<>(); 10 | public SkeletonMinutia(IntPoint position) { 11 | this.position = position; 12 | } 13 | public void attachStart(SkeletonRidge ridge) { 14 | if (!ridges.contains(ridge)) { 15 | ridges.add(ridge); 16 | ridge.start(this); 17 | } 18 | } 19 | public void detachStart(SkeletonRidge ridge) { 20 | if (ridges.contains(ridge)) { 21 | ridges.remove(ridge); 22 | if (ridge.start() == this) 23 | ridge.start(null); 24 | } 25 | } 26 | @Override 27 | public String toString() { 28 | return String.format("%s*%d", position.toString(), ridges.size()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/extractor/skeletons/SkeletonTailFilter.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.extractor.skeletons; 3 | 4 | import com.machinezoo.sourceafis.engine.configuration.*; 5 | import com.machinezoo.sourceafis.engine.features.*; 6 | import com.machinezoo.sourceafis.engine.transparency.*; 7 | 8 | public class SkeletonTailFilter { 9 | public static void apply(Skeleton skeleton) { 10 | for (SkeletonMinutia minutia : skeleton.minutiae) { 11 | if (minutia.ridges.size() == 1 && minutia.ridges.get(0).end().ridges.size() >= 3) 12 | if (minutia.ridges.get(0).points.size() < Parameters.MIN_TAIL_LENGTH) 13 | minutia.ridges.get(0).detach(); 14 | } 15 | SkeletonDotFilter.apply(skeleton); 16 | SkeletonKnotFilter.apply(skeleton); 17 | // https://sourceafis.machinezoo.com/transparency/removed-tails 18 | TransparencySink.current().logSkeleton("removed-tails", skeleton); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/extractor/minutiae/TopMinutiaeFilter.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.extractor.minutiae; 3 | 4 | import static java.util.stream.Collectors.*; 5 | import java.util.*; 6 | import com.machinezoo.sourceafis.engine.configuration.*; 7 | import com.machinezoo.sourceafis.engine.features.*; 8 | 9 | public class TopMinutiaeFilter { 10 | public static List apply(List minutiae) { 11 | if (minutiae.size() <= Parameters.MAX_MINUTIAE) 12 | return minutiae; 13 | return minutiae.stream() 14 | .sorted(Comparator.comparingInt( 15 | minutia -> minutiae.stream() 16 | .mapToInt(neighbor -> minutia.position.minus(neighbor.position).lengthSq()) 17 | .sorted() 18 | .skip(Parameters.SORT_BY_NEIGHBOR) 19 | .findFirst().orElse(Integer.MAX_VALUE)) 20 | .reversed()) 21 | .limit(Parameters.MAX_MINUTIAE) 22 | .collect(toList()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/primitives/BlockGrid.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.primitives; 3 | 4 | public class BlockGrid { 5 | public final IntPoint blocks; 6 | public final IntPoint corners; 7 | public final int[] x; 8 | public final int[] y; 9 | public BlockGrid(IntPoint size) { 10 | blocks = size; 11 | corners = new IntPoint(size.x + 1, size.y + 1); 12 | x = new int[size.x + 1]; 13 | y = new int[size.y + 1]; 14 | } 15 | public BlockGrid(int width, int height) { 16 | this(new IntPoint(width, height)); 17 | } 18 | public IntPoint corner(int atX, int atY) { 19 | return new IntPoint(x[atX], y[atY]); 20 | } 21 | public IntPoint corner(IntPoint at) { 22 | return corner(at.x, at.y); 23 | } 24 | public IntRect block(int atX, int atY) { 25 | return IntRect.between(corner(atX, atY), corner(atX + 1, atY + 1)); 26 | } 27 | public IntRect block(IntPoint at) { 28 | return block(at.x, at.y); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/primitives/IntMatrix.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.primitives; 3 | 4 | public class IntMatrix { 5 | public final int width; 6 | public final int height; 7 | private final int[] array; 8 | public IntMatrix(int width, int height) { 9 | this.width = width; 10 | this.height = height; 11 | array = new int[width * height]; 12 | } 13 | public IntMatrix(IntPoint size) { 14 | this(size.x, size.y); 15 | } 16 | public IntPoint size() { 17 | return new IntPoint(width, height); 18 | } 19 | public int get(int x, int y) { 20 | return array[offset(x, y)]; 21 | } 22 | public int get(IntPoint at) { 23 | return get(at.x, at.y); 24 | } 25 | public void set(int x, int y, int value) { 26 | array[offset(x, y)] = value; 27 | } 28 | public void set(IntPoint at, int value) { 29 | set(at.x, at.y, value); 30 | } 31 | private int offset(int x, int y) { 32 | return y * width + x; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute to SourceAFIS for Java 2 | 3 | Thank you for taking interest in SourceAFIS for Java. This document provides guidance for contributors. 4 | 5 | ## Repositories 6 | 7 | Sources are mirrored on several sites. You can submit issues and pull requests on any mirror. 8 | 9 | * [sourceafis-java @ GitHub](https://github.com/robertvazan/sourceafis-java) 10 | * [sourceafis-java @ Bitbucket](https://bitbucket.org/robertvazan/sourceafis-java) 11 | 12 | ## Issues 13 | 14 | Both bug reports and feature requests are welcome. There is no free support, but it's perfectly reasonable to open issues asking for more documentation or better usability. 15 | 16 | ## Pull requests 17 | 18 | Pull requests are generally welcome. If you would like to make large or controversial changes, open an issue first to discuss your idea. 19 | 20 | Don't worry about formatting and naming too much. Code will be reformatted after merge. Just don't run your formatter on whole source files, because it makes diffs hard to understand. 21 | 22 | ## License 23 | 24 | Your submissions will be distributed under [Apache License 2.0](LICENSE). 25 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/features/Skeleton.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.features; 3 | 4 | import java.util.*; 5 | import com.machinezoo.sourceafis.engine.primitives.*; 6 | 7 | public class Skeleton { 8 | public final SkeletonType type; 9 | public final IntPoint size; 10 | public final List minutiae = new ArrayList<>(); 11 | public Skeleton(SkeletonType type, IntPoint size) { 12 | this.type = type; 13 | this.size = size; 14 | } 15 | public void addMinutia(SkeletonMinutia minutia) { 16 | minutiae.add(minutia); 17 | } 18 | public void removeMinutia(SkeletonMinutia minutia) { 19 | minutiae.remove(minutia); 20 | } 21 | public BooleanMatrix shadow() { 22 | BooleanMatrix shadow = new BooleanMatrix(size); 23 | for (SkeletonMinutia minutia : minutiae) { 24 | shadow.set(minutia.position, true); 25 | for (SkeletonRidge ridge : minutia.ridges) 26 | if (ridge.start().position.y <= ridge.end().position.y) 27 | for (IntPoint point : ridge.points) 28 | shadow.set(point, true); 29 | } 30 | return shadow; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/matcher/MatcherThread.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.matcher; 3 | 4 | import java.util.*; 5 | 6 | public class MatcherThread { 7 | private static final ThreadLocal threads = new ThreadLocal() { 8 | /* 9 | * ThreadLocal has method withInitial() that is more convenient, 10 | * but that method alone would force whole SourceAFIS to require Android API level 26 instead of 24. 11 | */ 12 | @Override 13 | protected MatcherThread initialValue() { 14 | return new MatcherThread(); 15 | } 16 | }; 17 | public static MatcherThread current() { 18 | return threads.get(); 19 | } 20 | public static void kill() { 21 | threads.remove(); 22 | } 23 | public final MinutiaPairPool pool = new MinutiaPairPool(); 24 | public final RootList roots = new RootList(pool); 25 | public final PairingGraph pairing = new PairingGraph(pool); 26 | public final PriorityQueue queue = new PriorityQueue<>(Comparator.comparing(p -> p.distance)); 27 | public final ScoringData score = new ScoringData(); 28 | } 29 | -------------------------------------------------------------------------------- /src/test/java/com/machinezoo/sourceafis/engine/primitives/DoublePointTest.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.primitives; 3 | 4 | import static org.junit.jupiter.api.Assertions.*; 5 | import org.junit.jupiter.api.*; 6 | 7 | public class DoublePointTest { 8 | @Test 9 | public void constructor() { 10 | DoublePoint p = new DoublePoint(2.5, 3.5); 11 | assertEquals(2.5, p.x, 0.001); 12 | assertEquals(3.5, p.y, 0.001); 13 | } 14 | @Test 15 | public void add() { 16 | assertPointEquals(new DoublePoint(6, 8), new DoublePoint(2, 3).add(new DoublePoint(4, 5)), 0.001); 17 | } 18 | @Test 19 | public void multiply() { 20 | assertPointEquals(new DoublePoint(1, 1.5), new DoublePoint(2, 3).multiply(0.5), 0.001); 21 | } 22 | @Test 23 | public void round() { 24 | assertEquals(new IntPoint(2, 3), new DoublePoint(2.4, 2.6).round()); 25 | assertEquals(new IntPoint(-2, -3), new DoublePoint(-2.4, -2.6).round()); 26 | } 27 | static void assertPointEquals(DoublePoint expected, DoublePoint actual, double tolerance) { 28 | assertEquals(expected.x, actual.x, tolerance); 29 | assertEquals(expected.y, actual.y, tolerance); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/extractor/skeletons/SkeletonKnotFilter.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.extractor.skeletons; 3 | 4 | import com.machinezoo.sourceafis.engine.features.*; 5 | import com.machinezoo.sourceafis.engine.primitives.*; 6 | 7 | public class SkeletonKnotFilter { 8 | public static void apply(Skeleton skeleton) { 9 | for (SkeletonMinutia minutia : skeleton.minutiae) { 10 | if (minutia.ridges.size() == 2 && minutia.ridges.get(0).reversed != minutia.ridges.get(1)) { 11 | SkeletonRidge extended = minutia.ridges.get(0).reversed; 12 | SkeletonRidge removed = minutia.ridges.get(1); 13 | if (extended.points.size() < removed.points.size()) { 14 | SkeletonRidge tmp = extended; 15 | extended = removed; 16 | removed = tmp; 17 | extended = extended.reversed; 18 | removed = removed.reversed; 19 | } 20 | extended.points.remove(extended.points.size() - 1); 21 | for (IntPoint point : removed.points) 22 | extended.points.add(point); 23 | extended.end(removed.end()); 24 | removed.detach(); 25 | } 26 | } 27 | SkeletonDotFilter.apply(skeleton); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/primitives/BlockMap.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.primitives; 3 | 4 | public class BlockMap { 5 | public final IntPoint pixels; 6 | public final BlockGrid primary; 7 | public final BlockGrid secondary; 8 | public BlockMap(int width, int height, int maxBlockSize) { 9 | pixels = new IntPoint(width, height); 10 | primary = new BlockGrid(new IntPoint( 11 | Integers.roundUpDiv(pixels.x, maxBlockSize), 12 | Integers.roundUpDiv(pixels.y, maxBlockSize))); 13 | for (int y = 0; y <= primary.blocks.y; ++y) 14 | primary.y[y] = y * pixels.y / primary.blocks.y; 15 | for (int x = 0; x <= primary.blocks.x; ++x) 16 | primary.x[x] = x * pixels.x / primary.blocks.x; 17 | secondary = new BlockGrid(primary.corners); 18 | secondary.y[0] = 0; 19 | for (int y = 0; y < primary.blocks.y; ++y) 20 | secondary.y[y + 1] = primary.block(0, y).center().y; 21 | secondary.y[secondary.blocks.y] = pixels.y; 22 | secondary.x[0] = 0; 23 | for (int x = 0; x < primary.blocks.x; ++x) 24 | secondary.x[x + 1] = primary.block(x, 0).center().x; 25 | secondary.x[secondary.blocks.x] = pixels.x; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/transparency/ConsistentSkeleton.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.transparency; 3 | 4 | import static java.util.stream.Collectors.*; 5 | import java.util.*; 6 | import com.machinezoo.sourceafis.engine.features.*; 7 | import com.machinezoo.sourceafis.engine.primitives.*; 8 | 9 | public class ConsistentSkeleton { 10 | public final int width; 11 | public final int height; 12 | public final List minutiae; 13 | public final List ridges; 14 | public ConsistentSkeleton(Skeleton skeleton) { 15 | width = skeleton.size.x; 16 | height = skeleton.size.y; 17 | Map offsets = new HashMap<>(); 18 | for (int i = 0; i < skeleton.minutiae.size(); ++i) 19 | offsets.put(skeleton.minutiae.get(i), i); 20 | this.minutiae = skeleton.minutiae.stream().map(m -> m.position).collect(toList()); 21 | ridges = skeleton.minutiae.stream() 22 | .flatMap(m -> m.ridges.stream() 23 | .filter(r -> r.points instanceof CircularList) 24 | .map(r -> new ConsistentSkeletonRidge(offsets.get(r.start()), offsets.get(r.end()), r.points))) 25 | .collect(toList()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/primitives/FloatAngle.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.primitives; 3 | 4 | public class FloatAngle { 5 | public static final float PI2 = (float)DoubleAngle.PI2; 6 | public static final float PI = (float)Math.PI; 7 | public static final float HALF_PI = (float)DoubleAngle.HALF_PI; 8 | public static float add(float start, float delta) { 9 | float angle = start + delta; 10 | return angle < PI2 ? angle : angle - PI2; 11 | } 12 | public static float difference(float first, float second) { 13 | float angle = first - second; 14 | return angle >= 0 ? angle : angle + PI2; 15 | } 16 | public static float distance(float first, float second) { 17 | float delta = Math.abs(first - second); 18 | return delta <= PI ? delta : PI2 - delta; 19 | } 20 | public static float opposite(float angle) { 21 | return angle < PI ? angle + PI : angle - PI; 22 | } 23 | public static float complementary(float angle) { 24 | float complement = PI2 - angle; 25 | return complement < PI2 ? complement : complement - PI2; 26 | } 27 | public static boolean normalized(float angle) { 28 | return angle >= 0 && angle < PI2; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/images/AndroidBitmap.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.images; 3 | 4 | import java.lang.reflect.*; 5 | import com.machinezoo.noexception.*; 6 | 7 | class AndroidBitmap { 8 | static Class clazz = Exceptions.sneak().get(() -> Class.forName("android.graphics.Bitmap")); 9 | static Method getWidth = Exceptions.sneak().get(() -> clazz.getMethod("getWidth")); 10 | static Method getHeight = Exceptions.sneak().get(() -> clazz.getMethod("getHeight")); 11 | static Method getPixels = Exceptions.sneak().get(() -> clazz.getMethod("getPixels", int[].class, int.class, int.class, int.class, int.class, int.class, int.class)); 12 | final Object instance; 13 | AndroidBitmap(Object instance) { 14 | this.instance = instance; 15 | } 16 | int getWidth() { 17 | return Exceptions.sneak().getAsInt(() -> (int)getWidth.invoke(instance)); 18 | } 19 | int getHeight() { 20 | return Exceptions.sneak().getAsInt(() -> (int)getHeight.invoke(instance)); 21 | } 22 | void getPixels(int[] pixels, int offset, int stride, int x, int y, int width, int height) { 23 | Exceptions.sneak().run(() -> getPixels.invoke(instance, pixels, offset, stride, x, y, width, height)); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/test/java/com/machinezoo/sourceafis/engine/primitives/BlockGridTest.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.primitives; 3 | 4 | import static org.junit.jupiter.api.Assertions.*; 5 | import org.junit.jupiter.api.*; 6 | 7 | public class BlockGridTest { 8 | private final BlockGrid g = new BlockGrid(3, 4); 9 | public BlockGridTest() { 10 | for (int i = 0; i < g.x.length; ++i) 11 | g.x[i] = (i + 1) * 10; 12 | for (int i = 0; i < g.y.length; ++i) 13 | g.y[i] = (i + 1) * 100; 14 | } 15 | @Test 16 | public void constructor() { 17 | assertEquals(4, g.x.length); 18 | assertEquals(5, g.y.length); 19 | } 20 | @Test 21 | public void constructorFromPoint() { 22 | BlockGrid g = new BlockGrid(new IntPoint(2, 3)); 23 | assertEquals(3, g.x.length); 24 | assertEquals(4, g.y.length); 25 | } 26 | @Test 27 | public void cornerXY() { 28 | assertEquals(new IntPoint(20, 300), g.corner(1, 2)); 29 | } 30 | @Test 31 | public void cornerAt() { 32 | assertEquals(new IntPoint(10, 200), g.corner(new IntPoint(0, 1))); 33 | } 34 | @Test 35 | public void blockXY() { 36 | assertEquals(new IntRect(20, 300, 10, 100), g.block(1, 2)); 37 | } 38 | @Test 39 | public void blockAt() { 40 | assertEquals(new IntRect(10, 200, 10, 100), g.block(new IntPoint(0, 1))); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/images/AndroidImageDecoder.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.images; 3 | 4 | import com.machinezoo.sourceafis.engine.configuration.*; 5 | 6 | /* 7 | * This decoder uses Android's Bitmap class to decode templates. 8 | * Note that Bitmap class will not work in unit tests. It only works inside a full-blown emulator. 9 | * 10 | * Since direct references of Android libraries would not compile, 11 | * we will reference BitmapFactory and Bitmap via reflection. 12 | */ 13 | class AndroidImageDecoder extends ImageDecoder { 14 | @Override 15 | public boolean available() { 16 | return PlatformCheck.hasClass("android.graphics.BitmapFactory"); 17 | } 18 | @Override 19 | public String name() { 20 | return "Android"; 21 | } 22 | @Override 23 | public DecodedImage decode(byte[] image) { 24 | AndroidBitmap bitmap = AndroidBitmapFactory.decodeByteArray(image, 0, image.length); 25 | if (bitmap.instance == null) 26 | throw new IllegalArgumentException("Unsupported image format."); 27 | int width = bitmap.getWidth(); 28 | int height = bitmap.getHeight(); 29 | int[] pixels = new int[width * height]; 30 | bitmap.getPixels(pixels, 0, width, 0, 0, width, height); 31 | return new DecodedImage(width, height, pixels); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/test/java/com/machinezoo/sourceafis/engine/primitives/BlockMapTest.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.primitives; 3 | 4 | import static org.junit.jupiter.api.Assertions.*; 5 | import org.junit.jupiter.api.*; 6 | 7 | public class BlockMapTest { 8 | @Test 9 | public void constructor() { 10 | BlockMap m = new BlockMap(400, 600, 20); 11 | assertEquals(new IntPoint(400, 600), m.pixels); 12 | assertEquals(new IntPoint(20, 30), m.primary.blocks); 13 | assertEquals(new IntPoint(21, 31), m.primary.corners); 14 | assertEquals(new IntPoint(21, 31), m.secondary.blocks); 15 | assertEquals(new IntPoint(22, 32), m.secondary.corners); 16 | assertEquals(new IntPoint(0, 0), m.primary.corner(0, 0)); 17 | assertEquals(new IntPoint(400, 600), m.primary.corner(20, 30)); 18 | assertEquals(new IntPoint(200, 300), m.primary.corner(10, 15)); 19 | assertEquals(new IntRect(0, 0, 20, 20), m.primary.block(0, 0)); 20 | assertEquals(new IntRect(380, 580, 20, 20), m.primary.block(19, 29)); 21 | assertEquals(new IntRect(200, 300, 20, 20), m.primary.block(10, 15)); 22 | assertEquals(new IntRect(0, 0, 10, 10), m.secondary.block(0, 0)); 23 | assertEquals(new IntRect(390, 590, 10, 10), m.secondary.block(20, 30)); 24 | assertEquals(new IntRect(190, 290, 20, 20), m.secondary.block(10, 15)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/images/ImageIODecoder.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.images; 3 | 4 | import java.awt.image.*; 5 | import java.io.*; 6 | import javax.imageio.*; 7 | import com.machinezoo.noexception.*; 8 | import com.machinezoo.sourceafis.engine.configuration.*; 9 | 10 | /* 11 | * Image decoder using built-in ImageIO from JRE. 12 | * While ImageIO has its own extension mechanism, theoretically supporting any format, 13 | * this extension mechanism is cumbersome and on Android the whole ImageIO is missing. 14 | */ 15 | class ImageIODecoder extends ImageDecoder { 16 | @Override 17 | public boolean available() { 18 | return PlatformCheck.hasClass("javax.imageio.ImageIO"); 19 | } 20 | @Override 21 | public String name() { 22 | return "ImageIO"; 23 | } 24 | @Override 25 | public DecodedImage decode(byte[] image) { 26 | return Exceptions.sneak().get(() -> { 27 | BufferedImage buffered = ImageIO.read(new ByteArrayInputStream(image)); 28 | if (buffered == null) 29 | throw new IllegalArgumentException("Unsupported image format."); 30 | int width = buffered.getWidth(); 31 | int height = buffered.getHeight(); 32 | int[] pixels = new int[width * height]; 33 | buffered.getRGB(0, 0, width, height, pixels, 0, width); 34 | return new DecodedImage(width, height, pixels); 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/primitives/DoubleMatrix.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.primitives; 3 | 4 | public class DoubleMatrix { 5 | public final int width; 6 | public final int height; 7 | private final double[] cells; 8 | public DoubleMatrix(int width, int height) { 9 | this.width = width; 10 | this.height = height; 11 | cells = new double[width * height]; 12 | } 13 | public DoubleMatrix(IntPoint size) { 14 | this(size.x, size.y); 15 | } 16 | public IntPoint size() { 17 | return new IntPoint(width, height); 18 | } 19 | public double get(int x, int y) { 20 | return cells[offset(x, y)]; 21 | } 22 | public double get(IntPoint at) { 23 | return get(at.x, at.y); 24 | } 25 | public void set(int x, int y, double value) { 26 | cells[offset(x, y)] = value; 27 | } 28 | public void set(IntPoint at, double value) { 29 | set(at.x, at.y, value); 30 | } 31 | public void add(int x, int y, double value) { 32 | cells[offset(x, y)] += value; 33 | } 34 | public void add(IntPoint at, double value) { 35 | add(at.x, at.y, value); 36 | } 37 | public void multiply(int x, int y, double value) { 38 | cells[offset(x, y)] *= value; 39 | } 40 | public void multiply(IntPoint at, double value) { 41 | multiply(at.x, at.y, value); 42 | } 43 | private int offset(int x, int y) { 44 | return y * width + x; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/transparency/TransparencyZip.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.transparency; 3 | 4 | import java.io.*; 5 | import java.util.zip.*; 6 | import com.machinezoo.noexception.*; 7 | import com.machinezoo.sourceafis.*; 8 | 9 | public class TransparencyZip extends FingerprintTransparency { 10 | private final ZipOutputStream zip; 11 | private int offset; 12 | public TransparencyZip(OutputStream stream) { 13 | zip = new ZipOutputStream(stream); 14 | } 15 | /* 16 | * Synchronize take(), because ZipOutputStream can be accessed only from one thread 17 | * while transparency data may flow from multiple threads. 18 | */ 19 | @Override 20 | public synchronized void take(String key, String mime, byte[] data) { 21 | ++offset; 22 | /* 23 | * We allow providing custom output stream, which can fail at any moment. 24 | * We however also offer an API that is free of checked exceptions. 25 | * We will therefore wrap any checked exceptions from the output stream. 26 | */ 27 | Exceptions.wrap().run(() -> { 28 | zip.putNextEntry(new ZipEntry(String.format("%03d", offset) + "-" + key + TransparencyMimes.suffix(mime))); 29 | zip.write(data); 30 | zip.closeEntry(); 31 | }); 32 | } 33 | @Override 34 | public void close() { 35 | super.close(); 36 | Exceptions.wrap().run(zip::close); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/images/WsqDecoder.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.images; 3 | 4 | import org.jnbis.api.*; 5 | import org.jnbis.api.model.*; 6 | import com.machinezoo.noexception.*; 7 | 8 | /* 9 | * WSQ is often used to compress fingerprints, which is why JNBIS WSQ decoder is very valuable. 10 | */ 11 | class WsqDecoder extends ImageDecoder { 12 | @Override 13 | public boolean available() { 14 | /* 15 | * JNBIS WSQ decoder is pure Java, which means it is always available. 16 | */ 17 | return true; 18 | } 19 | @Override 20 | public String name() { 21 | return "WSQ"; 22 | } 23 | @Override 24 | public DecodedImage decode(byte[] image) { 25 | if (image.length < 2 || image[0] != (byte)0xff || image[1] != (byte)0xa0) 26 | throw new IllegalArgumentException("This is not a WSQ image."); 27 | return Exceptions.sneak().get(() -> { 28 | Bitmap bitmap = Jnbis.wsq().decode(image).asBitmap(); 29 | int width = bitmap.getWidth(); 30 | int height = bitmap.getHeight(); 31 | byte[] buffer = bitmap.getPixels(); 32 | int[] pixels = new int[width * height]; 33 | for (int y = 0; y < height; ++y) { 34 | for (int x = 0; x < width; ++x) { 35 | int gray = buffer[y * width + x] & 0xff; 36 | pixels[y * width + x] = 0xff00_0000 | (gray << 16) | (gray << 8) | gray; 37 | } 38 | } 39 | return new DecodedImage(width, height, pixels); 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/extractor/ClippedContrast.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.extractor; 3 | 4 | import com.machinezoo.sourceafis.engine.configuration.*; 5 | import com.machinezoo.sourceafis.engine.primitives.*; 6 | import com.machinezoo.sourceafis.engine.transparency.*; 7 | 8 | public class ClippedContrast { 9 | public static DoubleMatrix compute(BlockMap blocks, HistogramCube histogram) { 10 | DoubleMatrix result = new DoubleMatrix(blocks.primary.blocks); 11 | for (IntPoint block : blocks.primary.blocks) { 12 | int volume = histogram.sum(block); 13 | int clipLimit = (int)Math.round(volume * Parameters.CLIPPED_CONTRAST); 14 | int accumulator = 0; 15 | int lowerBound = histogram.bins - 1; 16 | for (int i = 0; i < histogram.bins; ++i) { 17 | accumulator += histogram.get(block, i); 18 | if (accumulator > clipLimit) { 19 | lowerBound = i; 20 | break; 21 | } 22 | } 23 | accumulator = 0; 24 | int upperBound = 0; 25 | for (int i = histogram.bins - 1; i >= 0; --i) { 26 | accumulator += histogram.get(block, i); 27 | if (accumulator > clipLimit) { 28 | upperBound = i; 29 | break; 30 | } 31 | } 32 | result.set(block, (upperBound - lowerBound) * (1.0 / (histogram.bins - 1))); 33 | } 34 | // https://sourceafis.machinezoo.com/transparency/contrast 35 | TransparencySink.current().log("contrast", result); 36 | return result; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/extractor/RelativeContrastMask.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.extractor; 3 | 4 | import java.util.*; 5 | import com.machinezoo.sourceafis.engine.configuration.*; 6 | import com.machinezoo.sourceafis.engine.primitives.*; 7 | import com.machinezoo.sourceafis.engine.transparency.*; 8 | 9 | public class RelativeContrastMask { 10 | public static BooleanMatrix compute(DoubleMatrix contrast, BlockMap blocks) { 11 | List sortedContrast = new ArrayList<>(); 12 | for (IntPoint block : contrast.size()) 13 | sortedContrast.add(contrast.get(block)); 14 | sortedContrast.sort(Comparator.naturalOrder().reversed()); 15 | int pixelsPerBlock = blocks.pixels.area() / blocks.primary.blocks.area(); 16 | int sampleCount = Math.min(sortedContrast.size(), Parameters.RELATIVE_CONTRAST_SAMPLE / pixelsPerBlock); 17 | int consideredBlocks = Math.max((int)Math.round(sampleCount * Parameters.RELATIVE_CONTRAST_PERCENTILE), 1); 18 | double averageContrast = sortedContrast.stream().mapToDouble(n -> n).limit(consideredBlocks).average().getAsDouble(); 19 | double limit = averageContrast * Parameters.MIN_RELATIVE_CONTRAST; 20 | BooleanMatrix result = new BooleanMatrix(blocks.primary.blocks); 21 | for (IntPoint block : blocks.primary.blocks) 22 | if (contrast.get(block) < limit) 23 | result.set(block, true); 24 | // https://sourceafis.machinezoo.com/transparency/relative-contrast-mask 25 | TransparencySink.current().log("relative-contrast-mask", result); 26 | return result; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/primitives/DoublePointMatrix.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.primitives; 3 | 4 | public class DoublePointMatrix { 5 | public final int width; 6 | public final int height; 7 | private final double[] vectors; 8 | public DoublePointMatrix(int width, int height) { 9 | this.width = width; 10 | this.height = height; 11 | vectors = new double[2 * width * height]; 12 | } 13 | public DoublePointMatrix(IntPoint size) { 14 | this(size.x, size.y); 15 | } 16 | public IntPoint size() { 17 | return new IntPoint(width, height); 18 | } 19 | public DoublePoint get(int x, int y) { 20 | int i = offset(x, y); 21 | return new DoublePoint(vectors[i], vectors[i + 1]); 22 | } 23 | public DoublePoint get(IntPoint at) { 24 | return get(at.x, at.y); 25 | } 26 | public void set(int x, int y, double px, double py) { 27 | int i = offset(x, y); 28 | vectors[i] = px; 29 | vectors[i + 1] = py; 30 | } 31 | public void set(int x, int y, DoublePoint point) { 32 | set(x, y, point.x, point.y); 33 | } 34 | public void set(IntPoint at, DoublePoint point) { 35 | set(at.x, at.y, point); 36 | } 37 | public void add(int x, int y, double px, double py) { 38 | int i = offset(x, y); 39 | vectors[i] += px; 40 | vectors[i + 1] += py; 41 | } 42 | public void add(int x, int y, DoublePoint point) { 43 | add(x, y, point.x, point.y); 44 | } 45 | public void add(IntPoint at, DoublePoint point) { 46 | add(at.x, at.y, point); 47 | } 48 | private int offset(int x, int y) { 49 | return 2 * (y * width + x); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/test/java/com/machinezoo/sourceafis/FingerprintMatcherTest.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis; 3 | 4 | import static org.hamcrest.MatcherAssert.*; 5 | import static org.hamcrest.Matchers.*; 6 | import org.junit.jupiter.api.*; 7 | 8 | public class FingerprintMatcherTest { 9 | private void matching(FingerprintTemplate probe, FingerprintTemplate candidate) { 10 | double score = new FingerprintMatcher(probe) 11 | .match(candidate); 12 | assertThat(score, greaterThan(40.0)); 13 | } 14 | private void nonmatching(FingerprintTemplate probe, FingerprintTemplate candidate) { 15 | double score = new FingerprintMatcher(probe) 16 | .match(candidate); 17 | assertThat(score, lessThan(20.0)); 18 | } 19 | @Test 20 | public void matchingPair() { 21 | matching(FingerprintTemplateTest.probe(), FingerprintTemplateTest.matching()); 22 | } 23 | @Test 24 | public void nonmatchingPair() { 25 | nonmatching(FingerprintTemplateTest.probe(), FingerprintTemplateTest.nonmatching()); 26 | } 27 | @Test 28 | public void matchingIso() { 29 | matching(FingerprintCompatibilityTest.probeIso(), FingerprintCompatibilityTest.matchingIso()); 30 | } 31 | @Test 32 | public void nonmatchingIso() { 33 | nonmatching(FingerprintCompatibilityTest.probeIso(), FingerprintCompatibilityTest.nonmatchingIso()); 34 | } 35 | @Test 36 | public void matchingGray() { 37 | matching(FingerprintTemplateTest.probeGray(), FingerprintTemplateTest.matchingGray()); 38 | } 39 | @Test 40 | public void nonmatchingGray() { 41 | nonmatching(FingerprintTemplateTest.probeGray(), FingerprintTemplateTest.nonmatchingGray()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/extractor/ImageResizer.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.extractor; 3 | 4 | import com.machinezoo.sourceafis.engine.primitives.*; 5 | 6 | public class ImageResizer { 7 | private static DoubleMatrix resize(DoubleMatrix input, int newWidth, int newHeight) { 8 | if (newWidth == input.width && newHeight == input.height) 9 | return input; 10 | DoubleMatrix output = new DoubleMatrix(newWidth, newHeight); 11 | double scaleX = newWidth / (double)input.width; 12 | double scaleY = newHeight / (double)input.height; 13 | double descaleX = 1 / scaleX; 14 | double descaleY = 1 / scaleY; 15 | for (int y = 0; y < newHeight; ++y) { 16 | double y1 = y * descaleY; 17 | double y2 = y1 + descaleY; 18 | int y1i = (int)y1; 19 | int y2i = Math.min((int)Math.ceil(y2), input.height); 20 | for (int x = 0; x < newWidth; ++x) { 21 | double x1 = x * descaleX; 22 | double x2 = x1 + descaleX; 23 | int x1i = (int)x1; 24 | int x2i = Math.min((int)Math.ceil(x2), input.width); 25 | double sum = 0; 26 | for (int oy = y1i; oy < y2i; ++oy) { 27 | double ry = Math.min(oy + 1, y2) - Math.max(oy, y1); 28 | for (int ox = x1i; ox < x2i; ++ox) { 29 | double rx = Math.min(ox + 1, x2) - Math.max(ox, x1); 30 | sum += rx * ry * input.get(ox, oy); 31 | } 32 | } 33 | output.set(x, y, sum * (scaleX * scaleY)); 34 | } 35 | } 36 | return output; 37 | } 38 | public static DoubleMatrix resize(DoubleMatrix input, double dpi) { 39 | return resize(input, (int)Math.round(500.0 / dpi * input.width), (int)Math.round(500.0 / dpi * input.height)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/matcher/Probe.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.matcher; 3 | 4 | import java.util.*; 5 | import com.machinezoo.sourceafis.engine.features.*; 6 | import com.machinezoo.sourceafis.engine.primitives.*; 7 | import com.machinezoo.sourceafis.engine.templates.*; 8 | import it.unimi.dsi.fastutil.ints.*; 9 | 10 | public class Probe { 11 | public static final Probe NULL = new Probe(); 12 | public final SearchTemplate template; 13 | public final Int2ObjectMap> hash; 14 | private Probe() { 15 | template = SearchTemplate.EMPTY; 16 | hash = new Int2ObjectOpenHashMap<>(); 17 | } 18 | public Probe(SearchTemplate template, Int2ObjectMap> edgeHash) { 19 | this.template = template; 20 | this.hash = edgeHash; 21 | } 22 | public int memory() { 23 | return MemoryEstimates.object(2 * MemoryEstimates.REFERENCE, MemoryEstimates.REFERENCE) 24 | + template.memory() 25 | + MemoryEstimates.object(10 * MemoryEstimates.REFERENCE, MemoryEstimates.REFERENCE) 26 | + MemoryEstimates.array(Integer.BYTES, hash.size() * 3 / 2) 27 | + MemoryEstimates.array(MemoryEstimates.REFERENCE, hash.size() * 3 / 2) 28 | + hash.values().stream() 29 | .mapToInt(list -> MemoryEstimates.object(Integer.BYTES + MemoryEstimates.REFERENCE, MemoryEstimates.REFERENCE) 30 | + MemoryEstimates.array(MemoryEstimates.REFERENCE, Math.max(10, list.size() * 3 / 2)) 31 | + list.size() * IndexedEdge.memory()) 32 | .sum(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/extractor/skeletons/SkeletonPoreFilter.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.extractor.skeletons; 3 | 4 | import com.machinezoo.sourceafis.engine.configuration.*; 5 | import com.machinezoo.sourceafis.engine.features.*; 6 | import com.machinezoo.sourceafis.engine.primitives.*; 7 | import com.machinezoo.sourceafis.engine.transparency.*; 8 | 9 | public class SkeletonPoreFilter { 10 | public static void apply(Skeleton skeleton) { 11 | for (SkeletonMinutia minutia : skeleton.minutiae) { 12 | if (minutia.ridges.size() == 3) { 13 | for (int exit = 0; exit < 3; ++exit) { 14 | SkeletonRidge exitRidge = minutia.ridges.get(exit); 15 | SkeletonRidge arm1 = minutia.ridges.get((exit + 1) % 3); 16 | SkeletonRidge arm2 = minutia.ridges.get((exit + 2) % 3); 17 | if (arm1.end() == arm2.end() && exitRidge.end() != arm1.end() && arm1.end() != minutia && exitRidge.end() != minutia) { 18 | SkeletonMinutia end = arm1.end(); 19 | if (end.ridges.size() == 3 && arm1.points.size() <= Parameters.MAX_PORE_ARM && arm2.points.size() <= Parameters.MAX_PORE_ARM) { 20 | arm1.detach(); 21 | arm2.detach(); 22 | SkeletonRidge merged = new SkeletonRidge(); 23 | merged.start(minutia); 24 | merged.end(end); 25 | for (IntPoint point : minutia.position.lineTo(end.position)) 26 | merged.points.add(point); 27 | } 28 | break; 29 | } 30 | } 31 | } 32 | } 33 | SkeletonKnotFilter.apply(skeleton); 34 | // https://sourceafis.machinezoo.com/transparency/removed-pores 35 | TransparencySink.current().logSkeleton("removed-pores", skeleton); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/com/machinezoo/sourceafis/TestResources.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis; 3 | 4 | import java.io.*; 5 | import org.apache.commons.io.*; 6 | import com.machinezoo.noexception.*; 7 | 8 | public class TestResources { 9 | private static byte[] load(String name) { 10 | return Exceptions.sneak().get(() -> { 11 | try (InputStream input = TestResources.class.getResourceAsStream(name)) { 12 | return IOUtils.toByteArray(input); 13 | } 14 | }); 15 | } 16 | public static byte[] png() { 17 | return load("probe.png"); 18 | } 19 | public static byte[] jpeg() { 20 | return load("probe.jpeg"); 21 | } 22 | public static byte[] bmp() { 23 | return load("probe.bmp"); 24 | } 25 | public static byte[] originalWsq() { 26 | return load("wsq-original.wsq"); 27 | } 28 | public static byte[] convertedWsq() { 29 | return load("wsq-converted.png"); 30 | } 31 | public static byte[] probe() { 32 | return load("probe.png"); 33 | } 34 | public static byte[] matching() { 35 | return load("matching.png"); 36 | } 37 | public static byte[] nonmatching() { 38 | return load("nonmatching.png"); 39 | } 40 | public static byte[] probeGray() { 41 | return load("gray-probe.dat"); 42 | } 43 | public static byte[] matchingGray() { 44 | return load("gray-matching.dat"); 45 | } 46 | public static byte[] nonmatchingGray() { 47 | return load("gray-nonmatching.dat"); 48 | } 49 | public static byte[] probeIso() { 50 | return load("iso-probe.dat"); 51 | } 52 | public static byte[] matchingIso() { 53 | return load("iso-matching.dat"); 54 | } 55 | public static byte[] nonmatchingIso() { 56 | return load("iso-nonmatching.dat"); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/FingerprintImageOptions.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis; 3 | 4 | /** 5 | * Additional information about fingerprint image. 6 | * {@code FingerprintImageOptions} can be passed to {@link FingerprintImage} constructor 7 | * to provide additional information about fingerprint image that supplements raw pixel data. 8 | * Since SourceAFIS algorithm is not scale-invariant, all images should have 9 | * DPI configured explicitly by calling {@link #dpi(double)}. 10 | * 11 | * @see FingerprintImage 12 | */ 13 | public class FingerprintImageOptions { 14 | /* 15 | * API roadmap: 16 | * + position(FingerprintPosition) 17 | * + other fingerprint properties 18 | */ 19 | double dpi = 500; 20 | /** 21 | * Initializes default options. 22 | * Call methods of this class to customize the options. 23 | */ 24 | public FingerprintImageOptions() { 25 | } 26 | /** 27 | * Sets image resolution. Resolution in measured in dots per inch (DPI). 28 | * SourceAFIS algorithm is not scale-invariant. Fingerprints with incorrectly configured DPI may fail to match. 29 | * Check your fingerprint reader specification for correct DPI value. Default DPI is 500. 30 | * 31 | * @param dpi 32 | * image resolution in DPI (dots per inch), usually around 500 33 | * @return {@code this} (fluent method) 34 | * @throws IllegalArgumentException 35 | * if {@code dpi} is non-positive, impossibly low, or impossibly high 36 | */ 37 | public FingerprintImageOptions dpi(double dpi) { 38 | if (dpi < 20 || dpi > 20_000) 39 | throw new IllegalArgumentException(); 40 | this.dpi = dpi; 41 | return this; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/primitives/HistogramCube.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.primitives; 3 | 4 | public class HistogramCube { 5 | public final int width; 6 | public final int height; 7 | public final int bins; 8 | private final int[] counts; 9 | public HistogramCube(int width, int height, int bins) { 10 | this.width = width; 11 | this.height = height; 12 | this.bins = bins; 13 | counts = new int[width * height * bins]; 14 | } 15 | public HistogramCube(IntPoint size, int bins) { 16 | this(size.x, size.y, bins); 17 | } 18 | public int constrain(int z) { 19 | return Math.max(0, Math.min(bins - 1, z)); 20 | } 21 | public int get(int x, int y, int z) { 22 | return counts[offset(x, y, z)]; 23 | } 24 | public int get(IntPoint at, int z) { 25 | return get(at.x, at.y, z); 26 | } 27 | public int sum(int x, int y) { 28 | int sum = 0; 29 | for (int i = 0; i < bins; ++i) 30 | sum += get(x, y, i); 31 | return sum; 32 | } 33 | public int sum(IntPoint at) { 34 | return sum(at.x, at.y); 35 | } 36 | public void set(int x, int y, int z, int value) { 37 | counts[offset(x, y, z)] = value; 38 | } 39 | public void set(IntPoint at, int z, int value) { 40 | set(at.x, at.y, z, value); 41 | } 42 | public void add(int x, int y, int z, int value) { 43 | counts[offset(x, y, z)] += value; 44 | } 45 | public void add(IntPoint at, int z, int value) { 46 | add(at.x, at.y, z, value); 47 | } 48 | public void increment(int x, int y, int z) { 49 | add(x, y, z, 1); 50 | } 51 | public void increment(IntPoint at, int z) { 52 | increment(at.x, at.y, z); 53 | } 54 | private int offset(int x, int y, int z) { 55 | return (y * width + x) * bins + z; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/primitives/BooleanMatrix.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.primitives; 3 | 4 | public class BooleanMatrix { 5 | public final int width; 6 | public final int height; 7 | private final boolean[] cells; 8 | public BooleanMatrix(int width, int height) { 9 | this.width = width; 10 | this.height = height; 11 | cells = new boolean[width * height]; 12 | } 13 | public BooleanMatrix(IntPoint size) { 14 | this(size.x, size.y); 15 | } 16 | public BooleanMatrix(BooleanMatrix other) { 17 | this(other.size()); 18 | for (int i = 0; i < cells.length; ++i) 19 | cells[i] = other.cells[i]; 20 | } 21 | public IntPoint size() { 22 | return new IntPoint(width, height); 23 | } 24 | public boolean get(int x, int y) { 25 | return cells[offset(x, y)]; 26 | } 27 | public boolean get(IntPoint at) { 28 | return get(at.x, at.y); 29 | } 30 | public boolean get(int x, int y, boolean fallback) { 31 | if (x < 0 || y < 0 || x >= width || y >= height) 32 | return fallback; 33 | return cells[offset(x, y)]; 34 | } 35 | public boolean get(IntPoint at, boolean fallback) { 36 | return get(at.x, at.y, fallback); 37 | } 38 | public void set(int x, int y, boolean value) { 39 | cells[offset(x, y)] = value; 40 | } 41 | public void set(IntPoint at, boolean value) { 42 | set(at.x, at.y, value); 43 | } 44 | public void invert() { 45 | for (int i = 0; i < cells.length; ++i) 46 | cells[i] = !cells[i]; 47 | } 48 | public void merge(BooleanMatrix other) { 49 | if (other.width != width || other.height != height) 50 | throw new IllegalArgumentException(); 51 | for (int i = 0; i < cells.length; ++i) 52 | cells[i] |= other.cells[i]; 53 | } 54 | private int offset(int x, int y) { 55 | return y * width + x; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/extractor/LocalHistograms.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.extractor; 3 | 4 | import com.machinezoo.sourceafis.engine.configuration.*; 5 | import com.machinezoo.sourceafis.engine.primitives.*; 6 | import com.machinezoo.sourceafis.engine.transparency.*; 7 | 8 | public class LocalHistograms { 9 | public static HistogramCube create(BlockMap blocks, DoubleMatrix image) { 10 | HistogramCube histogram = new HistogramCube(blocks.primary.blocks, Parameters.HISTOGRAM_DEPTH); 11 | for (IntPoint block : blocks.primary.blocks) { 12 | IntRect area = blocks.primary.block(block); 13 | for (int y = area.top(); y < area.bottom(); ++y) 14 | for (int x = area.left(); x < area.right(); ++x) { 15 | int depth = (int)(image.get(x, y) * histogram.bins); 16 | histogram.increment(block, histogram.constrain(depth)); 17 | } 18 | } 19 | // https://sourceafis.machinezoo.com/transparency/histogram 20 | TransparencySink.current().log("histogram", histogram); 21 | return histogram; 22 | } 23 | public static HistogramCube smooth(BlockMap blocks, HistogramCube input) { 24 | IntPoint[] blocksAround = new IntPoint[] { new IntPoint(0, 0), new IntPoint(-1, 0), new IntPoint(0, -1), new IntPoint(-1, -1) }; 25 | HistogramCube output = new HistogramCube(blocks.secondary.blocks, input.bins); 26 | for (IntPoint corner : blocks.secondary.blocks) { 27 | for (IntPoint relative : blocksAround) { 28 | IntPoint block = corner.plus(relative); 29 | if (blocks.primary.blocks.contains(block)) { 30 | for (int i = 0; i < input.bins; ++i) 31 | output.add(corner, i, input.get(block, i)); 32 | } 33 | } 34 | } 35 | // https://sourceafis.machinezoo.com/transparency/smoothed-histogram 36 | TransparencySink.current().log("smoothed-histogram", output); 37 | return output; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/matcher/RootEnumerator.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.matcher; 3 | 4 | import com.machinezoo.sourceafis.engine.configuration.*; 5 | import com.machinezoo.sourceafis.engine.features.*; 6 | import com.machinezoo.sourceafis.engine.templates.*; 7 | 8 | public class RootEnumerator { 9 | public static void enumerate(Probe probe, SearchTemplate candidate, RootList roots) { 10 | var cminutiae = candidate.minutiae; 11 | int lookups = 0; 12 | int tried = 0; 13 | for (boolean shortEdges : new boolean[] { false, true }) { 14 | for (int period = 1; period < cminutiae.length; ++period) { 15 | for (int phase = 0; phase <= period; ++phase) { 16 | for (int creference = phase; creference < cminutiae.length; creference += period + 1) { 17 | int cneighbor = (creference + period) % cminutiae.length; 18 | var cedge = new EdgeShape(cminutiae[creference], cminutiae[cneighbor]); 19 | if ((cedge.length >= Parameters.MIN_ROOT_EDGE_LENGTH) ^ shortEdges) { 20 | var matches = probe.hash.get(EdgeHashes.hash(cedge)); 21 | if (matches != null) { 22 | for (var match : matches) { 23 | if (EdgeHashes.matching(match, cedge)) { 24 | int duplicateKey = (match.reference() << 16) | creference; 25 | if (roots.duplicates.add(duplicateKey)) { 26 | MinutiaPair pair = roots.pool.allocate(); 27 | pair.probe = match.reference(); 28 | pair.candidate = creference; 29 | roots.add(pair); 30 | } 31 | ++tried; 32 | if (tried >= Parameters.MAX_TRIED_ROOTS) 33 | return; 34 | } 35 | } 36 | } 37 | ++lookups; 38 | if (lookups >= Parameters.MAX_ROOT_EDGE_LOOKUPS) 39 | return; 40 | } 41 | } 42 | } 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![SWUbanner](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg)](https://github.com/vshymanskyy/StandWithUkraine/blob/main/docs/README.md) 2 | 3 | # SourceAFIS for Java 4 | 5 | [![Maven Central](https://img.shields.io/maven-central/v/com.machinezoo.sourceafis/sourceafis)](https://central.sonatype.com/artifact/com.machinezoo.sourceafis/sourceafis) 6 | [![Build status](https://github.com/robertvazan/sourceafis-java/workflows/build/badge.svg)](https://github.com/robertvazan/sourceafis-java/actions/workflows/build.yml) 7 | [![Test coverage](https://codecov.io/gh/robertvazan/sourceafis-java/branch/master/graph/badge.svg)](https://codecov.io/gh/robertvazan/sourceafis-java) 8 | 9 | SourceAFIS for Java is a pure Java port of [SourceAFIS](https://sourceafis.machinezoo.com/), 10 | an algorithm for recognition of human fingerprints. 11 | It can compare two fingerprints 1:1 or search a large database 1:N for matching fingerprint. 12 | It takes fingerprint images on input and produces similarity score on output. 13 | Similarity score is then compared to customizable match threshold. 14 | 15 | More on [homepage](https://sourceafis.machinezoo.com/java). 16 | 17 | ## Status 18 | 19 | Stable and maintained. [Stagean](https://stagean.machinezoo.com/) is used to track progress on class and method level. 20 | 21 | ## Getting started 22 | 23 | See [homepage](https://sourceafis.machinezoo.com/java). 24 | 25 | ## Documentation 26 | 27 | * [SourceAFIS for Java](https://sourceafis.machinezoo.com/java) 28 | * [Javadoc](https://sourceafis.machinezoo.com/javadoc/com.machinezoo.sourceafis/com/machinezoo/sourceafis/package-summary.html) 29 | * [SourceAFIS overview](https://sourceafis.machinezoo.com/) 30 | * [Algorithm](https://sourceafis.machinezoo.com/algorithm) 31 | 32 | ## Feedback 33 | 34 | Bug reports and pull requests are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md). 35 | 36 | ## License 37 | 38 | Distributed under [Apache License 2.0](LICENSE). 39 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/features/SkeletonRidge.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.features; 3 | 4 | import java.util.*; 5 | import com.machinezoo.sourceafis.engine.configuration.*; 6 | import com.machinezoo.sourceafis.engine.primitives.*; 7 | 8 | public class SkeletonRidge { 9 | public final List points; 10 | public final SkeletonRidge reversed; 11 | private SkeletonMinutia startMinutia; 12 | private SkeletonMinutia endMinutia; 13 | public SkeletonRidge() { 14 | points = new CircularList<>(); 15 | reversed = new SkeletonRidge(this); 16 | } 17 | public SkeletonRidge(SkeletonRidge reversed) { 18 | points = new ReversedList<>(reversed.points); 19 | this.reversed = reversed; 20 | } 21 | public SkeletonMinutia start() { 22 | return startMinutia; 23 | } 24 | public void start(SkeletonMinutia value) { 25 | if (startMinutia != value) { 26 | if (startMinutia != null) { 27 | SkeletonMinutia detachFrom = startMinutia; 28 | startMinutia = null; 29 | detachFrom.detachStart(this); 30 | } 31 | startMinutia = value; 32 | if (startMinutia != null) 33 | startMinutia.attachStart(this); 34 | reversed.endMinutia = value; 35 | } 36 | } 37 | public SkeletonMinutia end() { 38 | return endMinutia; 39 | } 40 | public void end(SkeletonMinutia value) { 41 | if (endMinutia != value) { 42 | endMinutia = value; 43 | reversed.start(value); 44 | } 45 | } 46 | public void detach() { 47 | start(null); 48 | end(null); 49 | } 50 | public float direction() { 51 | int first = Parameters.RIDGE_DIRECTION_SKIP; 52 | int last = Parameters.RIDGE_DIRECTION_SKIP + Parameters.RIDGE_DIRECTION_SAMPLE - 1; 53 | if (last >= points.size()) { 54 | int shift = last - points.size() + 1; 55 | last -= shift; 56 | first -= shift; 57 | } 58 | if (first < 0) 59 | first = 0; 60 | return (float)DoubleAngle.atan(points.get(first), points.get(last)); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/test/java/com/machinezoo/sourceafis/engine/primitives/DoublesTest.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.primitives; 3 | 4 | import static org.junit.jupiter.api.Assertions.*; 5 | import org.junit.jupiter.api.*; 6 | 7 | public class DoublesTest { 8 | @Test 9 | public void sq() { 10 | assertEquals(6.25, Doubles.sq(2.5), 0.001); 11 | assertEquals(6.25, Doubles.sq(-2.5), 0.001); 12 | } 13 | @Test 14 | public void interpolate1D() { 15 | assertEquals(5, Doubles.interpolate(3, 7, 0.5), 0.001); 16 | assertEquals(3, Doubles.interpolate(3, 7, 0), 0.001); 17 | assertEquals(7, Doubles.interpolate(3, 7, 1), 0.001); 18 | assertEquals(6, Doubles.interpolate(7, 3, 0.25), 0.001); 19 | assertEquals(11, Doubles.interpolate(7, 3, -1), 0.001); 20 | assertEquals(9, Doubles.interpolate(3, 7, 1.5), 0.001); 21 | } 22 | @Test 23 | public void interpolate2D() { 24 | assertEquals(2, Doubles.interpolate(3, 7, 2, 4, 0, 0), 0.001); 25 | assertEquals(4, Doubles.interpolate(3, 7, 2, 4, 1, 0), 0.001); 26 | assertEquals(3, Doubles.interpolate(3, 7, 2, 4, 0, 1), 0.001); 27 | assertEquals(7, Doubles.interpolate(3, 7, 2, 4, 1, 1), 0.001); 28 | assertEquals(2.5, Doubles.interpolate(3, 7, 2, 4, 0, 0.5), 0.001); 29 | assertEquals(5.5, Doubles.interpolate(3, 7, 2, 4, 1, 0.5), 0.001); 30 | assertEquals(3, Doubles.interpolate(3, 7, 2, 4, 0.5, 0), 0.001); 31 | assertEquals(5, Doubles.interpolate(3, 7, 2, 4, 0.5, 1), 0.001); 32 | assertEquals(4, Doubles.interpolate(3, 7, 2, 4, 0.5, 0.5), 0.001); 33 | } 34 | @Test 35 | public void interpolateExponential() { 36 | assertEquals(3, Doubles.interpolateExponential(3, 10, 0), 0.001); 37 | assertEquals(10, Doubles.interpolateExponential(3, 10, 1), 0.001); 38 | assertEquals(3, Doubles.interpolateExponential(1, 9, 0.5), 0.001); 39 | assertEquals(27, Doubles.interpolateExponential(1, 9, 1.5), 0.001); 40 | assertEquals(1 / 3.0, Doubles.interpolateExponential(1, 9, -0.5), 0.001); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/test/java/com/machinezoo/sourceafis/engine/primitives/DoubleMatrixTest.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.primitives; 3 | 4 | import static org.junit.jupiter.api.Assertions.*; 5 | import org.junit.jupiter.api.*; 6 | 7 | public class DoubleMatrixTest { 8 | private final DoubleMatrix m = new DoubleMatrix(3, 4); 9 | public DoubleMatrixTest() { 10 | for (int x = 0; x < m.width; ++x) 11 | for (int y = 0; y < m.height; ++y) 12 | m.set(x, y, 10 * x + y); 13 | } 14 | @Test 15 | public void constructor() { 16 | assertEquals(3, m.width); 17 | assertEquals(4, m.height); 18 | } 19 | @Test 20 | public void constructorFromPoint() { 21 | DoubleMatrix m = new DoubleMatrix(new IntPoint(3, 4)); 22 | assertEquals(3, m.width); 23 | assertEquals(4, m.height); 24 | } 25 | @Test 26 | public void size() { 27 | assertEquals(3, m.size().x); 28 | assertEquals(4, m.size().y); 29 | } 30 | @Test 31 | public void get() { 32 | assertEquals(12, m.get(1, 2), 0.001); 33 | assertEquals(21, m.get(2, 1), 0.001); 34 | } 35 | @Test 36 | public void getAt() { 37 | assertEquals(3, m.get(new IntPoint(0, 3)), 0.001); 38 | assertEquals(22, m.get(new IntPoint(2, 2)), 0.001); 39 | } 40 | @Test 41 | public void set() { 42 | m.set(1, 2, 101); 43 | assertEquals(101, m.get(1, 2), 0.001); 44 | } 45 | @Test 46 | public void setAt() { 47 | m.set(new IntPoint(2, 3), 101); 48 | assertEquals(101, m.get(2, 3), 0.001); 49 | } 50 | @Test 51 | public void add() { 52 | m.add(2, 1, 100); 53 | assertEquals(121, m.get(2, 1), 0.001); 54 | } 55 | @Test 56 | public void addAt() { 57 | m.add(new IntPoint(2, 3), 100); 58 | assertEquals(123, m.get(2, 3), 0.001); 59 | } 60 | @Test 61 | public void multiply() { 62 | m.multiply(1, 3, 10); 63 | assertEquals(130, m.get(1, 3), 0.001); 64 | } 65 | @Test 66 | public void multiplyAt() { 67 | m.multiply(new IntPoint(1, 2), 10); 68 | assertEquals(120, m.get(1, 2), 0.001); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | import com.machinezoo.stagean.*; 3 | 4 | /** 5 | * Java implementation of SourceAFIS fingerprint recognition algorithm. 6 | * See {@link com.machinezoo.sourceafis} package. 7 | */ 8 | @CodeIssue("Integrate prototype code into public library.") 9 | module com.machinezoo.sourceafis { 10 | exports com.machinezoo.sourceafis; 11 | /* 12 | * We only need ImageIO from the whole desktop module. 13 | */ 14 | requires java.desktop; 15 | requires com.machinezoo.stagean; 16 | /* 17 | * Transitive, because FingerprintTransparency implements it. 18 | */ 19 | requires transitive com.machinezoo.closeablescope; 20 | /* 21 | * Transitive, because we expose ExceptionHandler in the API. 22 | */ 23 | requires transitive com.machinezoo.noexception; 24 | /* 25 | * Transitive, because we are using FingerprintIO types in the API. 26 | * It's just TemplateFormat at the moment, but it could be expanded with foreign template options in the future. 27 | */ 28 | requires transitive com.machinezoo.fingerprintio; 29 | /* 30 | * Needed for setVisibility(PropertyAccessor.FIELD, Visibility.ANY). 31 | */ 32 | requires com.fasterxml.jackson.annotation; 33 | requires com.fasterxml.jackson.databind; 34 | requires com.fasterxml.jackson.dataformat.cbor; 35 | /* 36 | * Gson is only used by deprecated JSON serialization of templates. 37 | */ 38 | requires com.google.gson; 39 | requires it.unimi.dsi.fastutil; 40 | requires org.apache.commons.io; 41 | requires com.github.mhshams.jnbis; 42 | /* 43 | * Serialization needs reflection access. 44 | */ 45 | opens com.machinezoo.sourceafis.engine.templates to com.fasterxml.jackson.databind, com.google.gson; 46 | opens com.machinezoo.sourceafis.engine.primitives to com.fasterxml.jackson.databind, com.google.gson; 47 | opens com.machinezoo.sourceafis.engine.features to com.fasterxml.jackson.databind, com.google.gson; 48 | opens com.machinezoo.sourceafis.engine.transparency to com.fasterxml.jackson.databind; 49 | opens com.machinezoo.sourceafis.engine.matcher to com.fasterxml.jackson.databind; 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/features/EdgeShape.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.features; 3 | 4 | import com.machinezoo.sourceafis.engine.primitives.*; 5 | 6 | public class EdgeShape { 7 | private static final int POLAR_CACHE_BITS = 8; 8 | private static final int POLAR_CACHE_RADIUS = 1 << POLAR_CACHE_BITS; 9 | private static final int[] POLAR_DISTANCE_CACHE = new int[Integers.sq(POLAR_CACHE_RADIUS)]; 10 | private static final float[] POLAR_ANGLE_CACHE = new float[Integers.sq(POLAR_CACHE_RADIUS)]; 11 | public final short length; 12 | public final float referenceAngle; 13 | public final float neighborAngle; 14 | static { 15 | for (int y = 0; y < POLAR_CACHE_RADIUS; ++y) 16 | for (int x = 0; x < POLAR_CACHE_RADIUS; ++x) { 17 | POLAR_DISTANCE_CACHE[y * POLAR_CACHE_RADIUS + x] = (int)Math.round(Math.sqrt(Integers.sq(x) + Integers.sq(y))); 18 | if (y > 0 || x > 0) 19 | POLAR_ANGLE_CACHE[y * POLAR_CACHE_RADIUS + x] = (float)DoubleAngle.atan(new DoublePoint(x, y)); 20 | else 21 | POLAR_ANGLE_CACHE[y * POLAR_CACHE_RADIUS + x] = 0; 22 | } 23 | } 24 | public EdgeShape(short length, float referenceAngle, float neighborAngle) { 25 | this.length = length; 26 | this.referenceAngle = referenceAngle; 27 | this.neighborAngle = neighborAngle; 28 | } 29 | public EdgeShape(SearchMinutia reference, SearchMinutia neighbor) { 30 | float quadrant = 0; 31 | int x = neighbor.x - reference.x; 32 | int y = neighbor.y - reference.y; 33 | if (y < 0) { 34 | x = -x; 35 | y = -y; 36 | quadrant = FloatAngle.PI; 37 | } 38 | if (x < 0) { 39 | int tmp = -x; 40 | x = y; 41 | y = tmp; 42 | quadrant += FloatAngle.HALF_PI; 43 | } 44 | int shift = 32 - Integer.numberOfLeadingZeros((x | y) >>> POLAR_CACHE_BITS); 45 | int offset = (y >> shift) * POLAR_CACHE_RADIUS + (x >> shift); 46 | length = (short)(POLAR_DISTANCE_CACHE[offset] << shift); 47 | float angle = POLAR_ANGLE_CACHE[offset] + quadrant; 48 | referenceAngle = FloatAngle.difference(reference.direction, angle); 49 | neighborAngle = FloatAngle.difference(neighbor.direction, FloatAngle.opposite(angle)); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/transparency/TransparencyMimes.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.transparency; 3 | 4 | import com.machinezoo.stagean.*; 5 | 6 | @DraftCode("Use some existing MIME library.") 7 | public class TransparencyMimes { 8 | /* 9 | * Specifying MIME type of the data allows construction of generic transparency data consumers. 10 | * For example, ZIP output for transparency data uses MIME type to assign file extension. 11 | * It is also possible to create generic transparency data browser that changes visualization based on MIME type. 12 | * 13 | * We will define short table mapping MIME types to file extensions, which is used by the ZIP implementation, 14 | * but it is currently also used to support the old API that used file extensions. 15 | * There are some MIME libraries out there, but no one was just right. 16 | * There are also public MIME type lists, but they have to be bundled and then kept up to date. 17 | * We will instead define only a short MIME type list covering data types we are likely to see here. 18 | */ 19 | public static String suffix(String mime) { 20 | switch (mime) { 21 | /* 22 | * Our primary serialization format. 23 | */ 24 | case "application/cbor": 25 | return ".cbor"; 26 | /* 27 | * Plain text for simple records. 28 | */ 29 | case "text/plain": 30 | return ".txt"; 31 | /* 32 | * Common serialization formats. 33 | */ 34 | case "application/json": 35 | return ".json"; 36 | case "application/xml": 37 | return ".xml"; 38 | /* 39 | * Image formats commonly used to encode fingerprints. 40 | */ 41 | case "image/jpeg": 42 | return ".jpeg"; 43 | case "image/png": 44 | return ".png"; 45 | case "image/bmp": 46 | return ".bmp"; 47 | case "image/tiff": 48 | return ".tiff"; 49 | case "image/jp2": 50 | return ".jp2"; 51 | /* 52 | * WSQ doesn't have a MIME type. We will invent one. 53 | */ 54 | case "image/x-wsq": 55 | return ".wsq"; 56 | /* 57 | * Fallback is needed, because there can be always some unexpected MIME type. 58 | */ 59 | default: 60 | return ".dat"; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/matcher/PairingGraph.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.matcher; 3 | 4 | import java.util.*; 5 | import com.machinezoo.sourceafis.engine.templates.*; 6 | 7 | public class PairingGraph { 8 | public final MinutiaPairPool pool; 9 | public int count; 10 | public MinutiaPair[] tree = new MinutiaPair[1]; 11 | public MinutiaPair[] byProbe = new MinutiaPair[1]; 12 | public MinutiaPair[] byCandidate = new MinutiaPair[1]; 13 | public final List support = new ArrayList<>(); 14 | public boolean supportEnabled; 15 | public PairingGraph(MinutiaPairPool pool) { 16 | this.pool = pool; 17 | } 18 | public void reserveProbe(Probe probe) { 19 | int capacity = probe.template.minutiae.length; 20 | if (capacity > tree.length) { 21 | tree = new MinutiaPair[capacity]; 22 | byProbe = new MinutiaPair[capacity]; 23 | } 24 | } 25 | public void reserveCandidate(SearchTemplate candidate) { 26 | int capacity = candidate.minutiae.length; 27 | if (byCandidate.length < capacity) 28 | byCandidate = new MinutiaPair[capacity]; 29 | } 30 | public void addPair(MinutiaPair pair) { 31 | tree[count] = pair; 32 | byProbe[pair.probe] = pair; 33 | byCandidate[pair.candidate] = pair; 34 | ++count; 35 | } 36 | public void support(MinutiaPair pair) { 37 | if (byProbe[pair.probe] != null && byProbe[pair.probe].candidate == pair.candidate) { 38 | ++byProbe[pair.probe].supportingEdges; 39 | ++byProbe[pair.probeRef].supportingEdges; 40 | if (supportEnabled) 41 | support.add(pair); 42 | else 43 | pool.release(pair); 44 | } else 45 | pool.release(pair); 46 | } 47 | public void clear() { 48 | for (int i = 0; i < count; ++i) { 49 | byProbe[tree[i].probe] = null; 50 | byCandidate[tree[i].candidate] = null; 51 | /* 52 | * Don't release root, just reset its supporting edge count. 53 | */ 54 | if (i > 0) 55 | pool.release(tree[i]); 56 | else 57 | tree[0].supportingEdges = 0; 58 | tree[i] = null; 59 | } 60 | count = 0; 61 | if (supportEnabled) { 62 | for (MinutiaPair pair : support) 63 | pool.release(pair); 64 | support.clear(); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/test/java/com/machinezoo/sourceafis/engine/primitives/HistogramCubeTest.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.primitives; 3 | 4 | import static org.junit.jupiter.api.Assertions.*; 5 | import org.junit.jupiter.api.*; 6 | 7 | public class HistogramCubeTest { 8 | private final HistogramCube h = new HistogramCube(4, 5, 6); 9 | public HistogramCubeTest() { 10 | for (int x = 0; x < h.width; ++x) 11 | for (int y = 0; y < h.height; ++y) 12 | for (int z = 0; z < h.bins; ++z) 13 | h.set(x, y, z, 100 * x + 10 * y + z); 14 | } 15 | @Test 16 | public void constructor() { 17 | assertEquals(4, h.width); 18 | assertEquals(5, h.height); 19 | assertEquals(6, h.bins); 20 | } 21 | @Test 22 | public void constrain() { 23 | assertEquals(3, h.constrain(3)); 24 | assertEquals(0, h.constrain(0)); 25 | assertEquals(5, h.constrain(5)); 26 | assertEquals(0, h.constrain(-1)); 27 | assertEquals(5, h.constrain(6)); 28 | } 29 | @Test 30 | public void get() { 31 | assertEquals(234, h.get(2, 3, 4)); 32 | assertEquals(312, h.get(3, 1, 2)); 33 | } 34 | @Test 35 | public void getAt() { 36 | assertEquals(125, h.get(new IntPoint(1, 2), 5)); 37 | assertEquals(243, h.get(new IntPoint(2, 4), 3)); 38 | } 39 | @Test 40 | public void sum() { 41 | assertEquals(6 * 120 + 1 + 2 + 3 + 4 + 5, h.sum(1, 2)); 42 | } 43 | @Test 44 | public void sumAt() { 45 | assertEquals(6 * 340 + 1 + 2 + 3 + 4 + 5, h.sum(new IntPoint(3, 4))); 46 | } 47 | @Test 48 | public void set() { 49 | h.set(2, 4, 3, 1000); 50 | assertEquals(1000, h.get(2, 4, 3)); 51 | } 52 | @Test 53 | public void setAt() { 54 | h.set(new IntPoint(3, 1), 5, 1000); 55 | assertEquals(1000, h.get(3, 1, 5)); 56 | } 57 | @Test 58 | public void add() { 59 | h.add(1, 2, 4, 1000); 60 | assertEquals(1124, h.get(1, 2, 4)); 61 | } 62 | @Test 63 | public void addAt() { 64 | h.add(new IntPoint(2, 4), 1, 1000); 65 | assertEquals(1241, h.get(2, 4, 1)); 66 | } 67 | @Test 68 | public void increment() { 69 | h.increment(3, 4, 1); 70 | assertEquals(342, h.get(3, 4, 1)); 71 | } 72 | @Test 73 | public void incrementAt() { 74 | h.increment(new IntPoint(2, 3), 5); 75 | assertEquals(236, h.get(2, 3, 5)); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/primitives/DoubleAngle.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.primitives; 3 | 4 | public class DoubleAngle { 5 | public static final double PI2 = 2 * Math.PI; 6 | public static final double INV_PI2 = 1.0 / PI2; 7 | public static final double HALF_PI = 0.5 * Math.PI; 8 | public static DoublePoint toVector(double angle) { 9 | return new DoublePoint(Math.cos(angle), Math.sin(angle)); 10 | } 11 | public static double atan(DoublePoint vector) { 12 | double angle = Math.atan2(vector.y, vector.x); 13 | return angle >= 0 ? angle : angle + PI2; 14 | } 15 | public static double atan(IntPoint vector) { 16 | return atan(vector.toDouble()); 17 | } 18 | public static double atan(IntPoint center, IntPoint point) { 19 | return atan(point.minus(center)); 20 | } 21 | public static double toOrientation(double angle) { 22 | return angle < Math.PI ? 2 * angle : 2 * (angle - Math.PI); 23 | } 24 | public static double fromOrientation(double angle) { 25 | return 0.5 * angle; 26 | } 27 | public static double add(double start, double delta) { 28 | double angle = start + delta; 29 | return angle < PI2 ? angle : angle - PI2; 30 | } 31 | public static double bucketCenter(int bucket, int resolution) { 32 | return PI2 * (2 * bucket + 1) / (2 * resolution); 33 | } 34 | public static int quantize(double angle, int resolution) { 35 | int result = (int)(angle * INV_PI2 * resolution); 36 | if (result < 0) 37 | return 0; 38 | else if (result >= resolution) 39 | return resolution - 1; 40 | else 41 | return result; 42 | } 43 | public static double opposite(double angle) { 44 | return angle < Math.PI ? angle + Math.PI : angle - Math.PI; 45 | } 46 | public static double distance(double first, double second) { 47 | double delta = Math.abs(first - second); 48 | return delta <= Math.PI ? delta : PI2 - delta; 49 | } 50 | public static double difference(double first, double second) { 51 | double angle = first - second; 52 | return angle >= 0 ? angle : angle + PI2; 53 | } 54 | public static double complementary(double angle) { 55 | double complement = PI2 - angle; 56 | return complement < PI2 ? complement : complement - PI2; 57 | } 58 | public static boolean normalized(double angle) { 59 | return angle >= 0 && angle < PI2; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/test/java/com/machinezoo/sourceafis/engine/primitives/DoublePointMatrixTest.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.primitives; 3 | 4 | import static org.junit.jupiter.api.Assertions.*; 5 | import org.junit.jupiter.api.*; 6 | 7 | public class DoublePointMatrixTest { 8 | private final DoublePointMatrix m = new DoublePointMatrix(4, 5); 9 | public DoublePointMatrixTest() { 10 | for (int x = 0; x < m.width; ++x) 11 | for (int y = 0; y < m.height; ++y) 12 | m.set(x, y, new DoublePoint(10 * x, 10 * y)); 13 | } 14 | @Test 15 | public void constructor() { 16 | assertEquals(4, m.width); 17 | assertEquals(5, m.height); 18 | } 19 | @Test 20 | public void constructorFromPoint() { 21 | DoublePointMatrix m = new DoublePointMatrix(new IntPoint(4, 5)); 22 | assertEquals(4, m.width); 23 | assertEquals(5, m.height); 24 | } 25 | @Test 26 | public void get() { 27 | DoublePointTest.assertPointEquals(new DoublePoint(20, 30), m.get(2, 3), 0.001); 28 | DoublePointTest.assertPointEquals(new DoublePoint(30, 10), m.get(3, 1), 0.001); 29 | } 30 | @Test 31 | public void getAt() { 32 | DoublePointTest.assertPointEquals(new DoublePoint(10, 20), m.get(new IntPoint(1, 2)), 0.001); 33 | DoublePointTest.assertPointEquals(new DoublePoint(20, 40), m.get(new IntPoint(2, 4)), 0.001); 34 | } 35 | @Test 36 | public void setValues() { 37 | m.set(2, 4, 101, 102); 38 | DoublePointTest.assertPointEquals(new DoublePoint(101, 102), m.get(2, 4), 0.001); 39 | } 40 | @Test 41 | public void set() { 42 | m.set(1, 2, new DoublePoint(101, 102)); 43 | DoublePointTest.assertPointEquals(new DoublePoint(101, 102), m.get(1, 2), 0.001); 44 | } 45 | @Test 46 | public void setAt() { 47 | m.set(new IntPoint(3, 2), new DoublePoint(101, 102)); 48 | DoublePointTest.assertPointEquals(new DoublePoint(101, 102), m.get(3, 2), 0.001); 49 | } 50 | @Test 51 | public void addValues() { 52 | m.add(3, 1, 100, 200); 53 | DoublePointTest.assertPointEquals(new DoublePoint(130, 210), m.get(3, 1), 0.001); 54 | } 55 | @Test 56 | public void add() { 57 | m.add(2, 3, new DoublePoint(100, 200)); 58 | DoublePointTest.assertPointEquals(new DoublePoint(120, 230), m.get(2, 3), 0.001); 59 | } 60 | @Test 61 | public void addAt() { 62 | m.add(new IntPoint(2, 4), new DoublePoint(100, 200)); 63 | DoublePointTest.assertPointEquals(new DoublePoint(120, 240), m.get(2, 4), 0.001); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/extractor/VoteFilter.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.extractor; 3 | 4 | import java.util.stream.*; 5 | import com.machinezoo.sourceafis.engine.primitives.*; 6 | 7 | public class VoteFilter { 8 | public static BooleanMatrix vote(BooleanMatrix input, BooleanMatrix mask, int radius, double majority, int borderDistance) { 9 | IntPoint size = input.size(); 10 | IntRect rect = new IntRect(borderDistance, borderDistance, size.x - 2 * borderDistance, size.y - 2 * borderDistance); 11 | int[] thresholds = IntStream.range(0, Integers.sq(2 * radius + 1) + 1).map(i -> (int)Math.ceil(majority * i)).toArray(); 12 | IntMatrix counts = new IntMatrix(size); 13 | BooleanMatrix output = new BooleanMatrix(size); 14 | for (int y = rect.top(); y < rect.bottom(); ++y) { 15 | int superTop = y - radius - 1; 16 | int superBottom = y + radius; 17 | int yMin = Math.max(0, y - radius); 18 | int yMax = Math.min(size.y - 1, y + radius); 19 | int yRange = yMax - yMin + 1; 20 | for (int x = rect.left(); x < rect.right(); ++x) 21 | if (mask == null || mask.get(x, y)) { 22 | int left = x > 0 ? counts.get(x - 1, y) : 0; 23 | int top = y > 0 ? counts.get(x, y - 1) : 0; 24 | int diagonal = x > 0 && y > 0 ? counts.get(x - 1, y - 1) : 0; 25 | int xMin = Math.max(0, x - radius); 26 | int xMax = Math.min(size.x - 1, x + radius); 27 | int ones; 28 | if (left > 0 && top > 0 && diagonal > 0) { 29 | ones = top + left - diagonal - 1; 30 | int superLeft = x - radius - 1; 31 | int superRight = x + radius; 32 | if (superLeft >= 0 && superTop >= 0 && input.get(superLeft, superTop)) 33 | ++ones; 34 | if (superLeft >= 0 && superBottom < size.y && input.get(superLeft, superBottom)) 35 | --ones; 36 | if (superRight < size.x && superTop >= 0 && input.get(superRight, superTop)) 37 | --ones; 38 | if (superRight < size.x && superBottom < size.y && input.get(superRight, superBottom)) 39 | ++ones; 40 | } else { 41 | ones = 0; 42 | for (int ny = yMin; ny <= yMax; ++ny) 43 | for (int nx = xMin; nx <= xMax; ++nx) 44 | if (input.get(nx, ny)) 45 | ++ones; 46 | } 47 | counts.set(x, y, ones + 1); 48 | if (ones >= thresholds[yRange * (xMax - xMin + 1)]) 49 | output.set(x, y, true); 50 | } 51 | } 52 | return output; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/extractor/BlockOrientations.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.extractor; 3 | 4 | import com.machinezoo.sourceafis.engine.configuration.*; 5 | import com.machinezoo.sourceafis.engine.primitives.*; 6 | import com.machinezoo.sourceafis.engine.transparency.*; 7 | 8 | public class BlockOrientations { 9 | private static DoublePointMatrix aggregate(DoublePointMatrix orientation, BlockMap blocks, BooleanMatrix mask) { 10 | DoublePointMatrix sums = new DoublePointMatrix(blocks.primary.blocks); 11 | for (IntPoint block : blocks.primary.blocks) { 12 | if (mask.get(block)) { 13 | IntRect area = blocks.primary.block(block); 14 | for (int y = area.top(); y < area.bottom(); ++y) 15 | for (int x = area.left(); x < area.right(); ++x) 16 | sums.add(block, orientation.get(x, y)); 17 | } 18 | } 19 | // https://sourceafis.machinezoo.com/transparency/block-orientation 20 | TransparencySink.current().log("block-orientation", sums); 21 | return sums; 22 | } 23 | private static DoublePointMatrix smooth(DoublePointMatrix orientation, BooleanMatrix mask) { 24 | IntPoint size = mask.size(); 25 | DoublePointMatrix smoothed = new DoublePointMatrix(size); 26 | for (IntPoint block : size) 27 | if (mask.get(block)) { 28 | IntRect neighbors = IntRect.around(block, Parameters.ORIENTATION_SMOOTHING_RADIUS).intersect(new IntRect(size)); 29 | for (int ny = neighbors.top(); ny < neighbors.bottom(); ++ny) 30 | for (int nx = neighbors.left(); nx < neighbors.right(); ++nx) 31 | if (mask.get(nx, ny)) 32 | smoothed.add(block, orientation.get(nx, ny)); 33 | } 34 | // https://sourceafis.machinezoo.com/transparency/smoothed-orientation 35 | TransparencySink.current().log("smoothed-orientation", smoothed); 36 | return smoothed; 37 | } 38 | private static DoubleMatrix angles(DoublePointMatrix vectors, BooleanMatrix mask) { 39 | IntPoint size = mask.size(); 40 | DoubleMatrix angles = new DoubleMatrix(size); 41 | for (IntPoint block : size) 42 | if (mask.get(block)) 43 | angles.set(block, DoubleAngle.atan(vectors.get(block))); 44 | return angles; 45 | } 46 | public static DoubleMatrix compute(DoubleMatrix image, BooleanMatrix mask, BlockMap blocks) { 47 | DoublePointMatrix accumulated = PixelwiseOrientations.compute(image, mask, blocks); 48 | DoublePointMatrix byBlock = aggregate(accumulated, blocks, mask); 49 | DoublePointMatrix smooth = smooth(byBlock, mask); 50 | return angles(smooth, mask); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/templates/SearchTemplate.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.templates; 3 | 4 | import static java.util.stream.Collectors.*; 5 | import java.util.*; 6 | import java.util.stream.*; 7 | import com.machinezoo.sourceafis.engine.features.*; 8 | import com.machinezoo.sourceafis.engine.primitives.*; 9 | import com.machinezoo.sourceafis.engine.transparency.*; 10 | 11 | public class SearchTemplate { 12 | public static final SearchTemplate EMPTY = new SearchTemplate(); 13 | public final short width; 14 | public final short height; 15 | public final SearchMinutia[] minutiae; 16 | public final NeighborEdge[][] edges; 17 | private SearchTemplate() { 18 | width = 1; 19 | height = 1; 20 | minutiae = new SearchMinutia[0]; 21 | edges = new NeighborEdge[0][]; 22 | } 23 | private static final int PRIME = 1610612741; 24 | public SearchTemplate(FeatureTemplate features) { 25 | width = (short)features.size.x; 26 | height = (short)features.size.y; 27 | minutiae = features.minutiae.stream() 28 | .map(SearchMinutia::new) 29 | .sorted(Comparator 30 | .comparingInt((SearchMinutia m) -> ((m.x * PRIME) + m.y) * PRIME) 31 | .thenComparingInt(m -> m.x) 32 | .thenComparingInt(m -> m.y) 33 | .thenComparingDouble(m -> m.direction) 34 | .thenComparing(m -> m.type)) 35 | .toArray(SearchMinutia[]::new); 36 | // https://sourceafis.machinezoo.com/transparency/shuffled-minutiae 37 | TransparencySink.current().log("shuffled-minutiae", this::features); 38 | edges = NeighborEdge.buildTable(minutiae); 39 | } 40 | public FeatureTemplate features() { 41 | return new FeatureTemplate(new IntPoint(width, height), Arrays.stream(minutiae).map(m -> m.feature()).collect(toList())); 42 | } 43 | public int memory() { 44 | return MemoryEstimates.object(2 * Short.BYTES + 2 * MemoryEstimates.REFERENCE, MemoryEstimates.REFERENCE) 45 | + MemoryEstimates.array(MemoryEstimates.REFERENCE, minutiae.length) 46 | + minutiae.length * SearchMinutia.memory() 47 | + MemoryEstimates.array(MemoryEstimates.REFERENCE, edges.length) 48 | + Stream.of(edges) 49 | .mapToInt(s -> MemoryEstimates.array(MemoryEstimates.REFERENCE, s.length) 50 | + s.length * NeighborEdge.memory()) 51 | .sum(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/primitives/CircularArray.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.primitives; 3 | 4 | class CircularArray { 5 | Object[] array; 6 | int head; 7 | int size; 8 | CircularArray(int capacity) { 9 | array = new Object[capacity]; 10 | } 11 | void validateItemIndex(int index) { 12 | if (index < 0 || index >= size) 13 | throw new IndexOutOfBoundsException(); 14 | } 15 | void validateCursorIndex(int index) { 16 | if (index < 0 || index > size) 17 | throw new IndexOutOfBoundsException(); 18 | } 19 | int location(int index) { 20 | return head + index < array.length ? head + index : head + index - array.length; 21 | } 22 | void enlarge() { 23 | Object[] enlarged = new Object[2 * array.length]; 24 | for (int i = 0; i < size; ++i) 25 | enlarged[i] = array[location(i)]; 26 | array = enlarged; 27 | head = 0; 28 | } 29 | Object get(int index) { 30 | validateItemIndex(index); 31 | return array[location(index)]; 32 | } 33 | void set(int index, Object item) { 34 | validateItemIndex(index); 35 | array[location(index)] = item; 36 | } 37 | void move(int from, int to, int length) { 38 | if (from < to) { 39 | for (int i = length - 1; i >= 0; --i) 40 | set(to + i, get(from + i)); 41 | } else if (from > to) { 42 | for (int i = 0; i < length; ++i) 43 | set(to + i, get(from + i)); 44 | } 45 | } 46 | void insert(int index, int amount) { 47 | validateCursorIndex(index); 48 | if (amount < 0) 49 | throw new IllegalArgumentException(); 50 | while (size + amount > array.length) 51 | enlarge(); 52 | if (2 * index >= size) { 53 | size += amount; 54 | move(index, index + amount, size - amount - index); 55 | } else { 56 | head -= amount; 57 | size += amount; 58 | if (head < 0) 59 | head += array.length; 60 | move(amount, 0, index); 61 | } 62 | for (int i = 0; i < amount; ++i) 63 | set(index + i, null); 64 | } 65 | void remove(int index, int amount) { 66 | validateCursorIndex(index); 67 | if (amount < 0) 68 | throw new IllegalArgumentException(); 69 | validateCursorIndex(index + amount); 70 | if (2 * index >= size - amount) { 71 | move(index + amount, index, size - amount - index); 72 | for (int i = 0; i < amount; ++i) 73 | set(size - i - 1, null); 74 | size -= amount; 75 | } else { 76 | move(0, amount, index); 77 | for (int i = 0; i < amount; ++i) 78 | set(i, null); 79 | head += amount; 80 | size -= amount; 81 | if (head >= array.length) 82 | head -= array.length; 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/test/java/com/machinezoo/sourceafis/FingerprintTransparencyTest.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis; 3 | 4 | import static org.hamcrest.MatcherAssert.*; 5 | import static org.hamcrest.Matchers.*; 6 | import java.util.*; 7 | import org.junit.jupiter.api.*; 8 | 9 | public class FingerprintTransparencyTest { 10 | private static class TransparencyChecker extends FingerprintTransparency { 11 | final List keys = new ArrayList<>(); 12 | @Override 13 | public void take(String key, String mime, byte[] data) { 14 | keys.add(key); 15 | assertThat(key, mime, is(oneOf("application/cbor", "text/plain"))); 16 | assertThat(key, data.length, greaterThan(0)); 17 | } 18 | } 19 | @Test 20 | public void versioned() { 21 | try (TransparencyChecker transparency = new TransparencyChecker()) { 22 | new FingerprintTemplate(FingerprintImageTest.probe()); 23 | assertThat(transparency.keys, hasItem("version")); 24 | } 25 | } 26 | @Test 27 | public void extractor() { 28 | try (TransparencyChecker transparency = new TransparencyChecker()) { 29 | new FingerprintTemplate(FingerprintImageTest.probe()); 30 | assertThat(transparency.keys, is(not(empty()))); 31 | } 32 | } 33 | @Test 34 | public void matcher() { 35 | FingerprintTemplate probe = FingerprintTemplateTest.probe(); 36 | FingerprintTemplate matching = FingerprintTemplateTest.matching(); 37 | new FingerprintTemplate(FingerprintImageTest.probe()); 38 | try (TransparencyChecker transparency = new TransparencyChecker()) { 39 | new FingerprintMatcher(probe) 40 | .match(matching); 41 | assertThat(transparency.keys, is(not(empty()))); 42 | } 43 | } 44 | @Test 45 | public void deserialization() { 46 | byte[] serialized = FingerprintTemplateTest.probe().toByteArray(); 47 | try (TransparencyChecker transparency = new TransparencyChecker()) { 48 | new FingerprintTemplate(serialized); 49 | assertThat(transparency.keys, is(not(empty()))); 50 | } 51 | } 52 | private static class TransparencyFilter extends FingerprintTransparency { 53 | final List keys = new ArrayList<>(); 54 | @Override 55 | public boolean accepts(String key) { 56 | return false; 57 | } 58 | @Override 59 | public void take(String key, String mime, byte[] data) { 60 | keys.add(key); 61 | } 62 | } 63 | @Test 64 | public void filtered() { 65 | try (TransparencyFilter transparency = new TransparencyFilter()) { 66 | new FingerprintMatcher(new FingerprintTemplate(FingerprintImageTest.probe())) 67 | .match(FingerprintTemplateTest.matching()); 68 | assertThat(transparency.keys, is(empty())); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/features/NeighborEdge.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.features; 3 | 4 | import java.util.*; 5 | import com.machinezoo.sourceafis.engine.configuration.*; 6 | import com.machinezoo.sourceafis.engine.primitives.*; 7 | import com.machinezoo.sourceafis.engine.transparency.*; 8 | 9 | public class NeighborEdge extends EdgeShape { 10 | public final short neighbor; 11 | public NeighborEdge(SearchMinutia[] minutiae, int reference, int neighbor) { 12 | super(minutiae[reference], minutiae[neighbor]); 13 | this.neighbor = (short)neighbor; 14 | } 15 | public static NeighborEdge[][] buildTable(SearchMinutia[] minutiae) { 16 | NeighborEdge[][] edges = new NeighborEdge[minutiae.length][]; 17 | List star = new ArrayList<>(); 18 | int[] allSqDistances = new int[minutiae.length]; 19 | for (int reference = 0; reference < edges.length; ++reference) { 20 | var rminutia = minutiae[reference]; 21 | int maxSqDistance = Integer.MAX_VALUE; 22 | if (minutiae.length - 1 > Parameters.EDGE_TABLE_NEIGHBORS) { 23 | for (int neighbor = 0; neighbor < minutiae.length; ++neighbor) { 24 | var nminutia = minutiae[neighbor]; 25 | allSqDistances[neighbor] = Integers.sq(rminutia.x - nminutia.x) + Integers.sq(rminutia.y - nminutia.y); 26 | } 27 | Arrays.sort(allSqDistances); 28 | maxSqDistance = allSqDistances[Parameters.EDGE_TABLE_NEIGHBORS]; 29 | } 30 | for (int neighbor = 0; neighbor < minutiae.length; ++neighbor) { 31 | var nminutia = minutiae[neighbor]; 32 | if (neighbor != reference && Integers.sq(rminutia.x - nminutia.x) + Integers.sq(rminutia.y - nminutia.y) <= maxSqDistance) 33 | star.add(new NeighborEdge(minutiae, reference, neighbor)); 34 | } 35 | star.sort(Comparator.comparingInt(e -> e.length).thenComparingInt(e -> e.neighbor)); 36 | while (star.size() > Parameters.EDGE_TABLE_NEIGHBORS) 37 | star.remove(star.size() - 1); 38 | edges[reference] = star.toArray(new NeighborEdge[star.size()]); 39 | star.clear(); 40 | } 41 | // https://sourceafis.machinezoo.com/transparency/edge-table 42 | TransparencySink.current().log("edge-table", edges); 43 | return edges; 44 | } 45 | public static int memory() { return MemoryEstimates.object(2 * Short.BYTES + 2 * Float.BYTES, Float.BYTES); } 46 | } 47 | -------------------------------------------------------------------------------- /src/test/java/com/machinezoo/sourceafis/FingerprintImageTest.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis; 3 | 4 | import static org.hamcrest.MatcherAssert.*; 5 | import static org.hamcrest.Matchers.*; 6 | import static org.junit.jupiter.api.Assertions.*; 7 | import org.junit.jupiter.api.*; 8 | import com.machinezoo.sourceafis.engine.primitives.*; 9 | 10 | public class FingerprintImageTest { 11 | @Test 12 | public void decodePNG() { 13 | new FingerprintImage(TestResources.png()); 14 | } 15 | private void assertSimilar(DoubleMatrix matrix, DoubleMatrix reference) { 16 | assertEquals(reference.width, matrix.width); 17 | assertEquals(reference.height, matrix.height); 18 | double delta = 0, max = -1, min = 1; 19 | for (int x = 0; x < matrix.width; ++x) { 20 | for (int y = 0; y < matrix.height; ++y) { 21 | delta += Math.abs(matrix.get(x, y) - reference.get(x, y)); 22 | max = Math.max(max, matrix.get(x, y)); 23 | min = Math.min(min, matrix.get(x, y)); 24 | } 25 | } 26 | assertTrue(max > 0.75); 27 | assertTrue(min < 0.1); 28 | assertTrue(delta / (matrix.width * matrix.height) < 0.01); 29 | } 30 | private void assertSimilar(byte[] image, byte[] reference) { 31 | assertSimilar(new FingerprintImage(image).matrix, new FingerprintImage(reference).matrix); 32 | } 33 | @Test 34 | public void decodeJPEG() { 35 | assertSimilar(TestResources.jpeg(), TestResources.png()); 36 | } 37 | @Test 38 | public void decodeBMP() { 39 | assertSimilar(TestResources.bmp(), TestResources.png()); 40 | } 41 | @Test 42 | public void decodeWSQ() { 43 | assertSimilar(TestResources.originalWsq(), TestResources.convertedWsq()); 44 | } 45 | public static FingerprintImage probe() { 46 | return new FingerprintImage(TestResources.probe()); 47 | } 48 | public static FingerprintImage matching() { 49 | return new FingerprintImage(TestResources.matching()); 50 | } 51 | public static FingerprintImage nonmatching() { 52 | return new FingerprintImage(TestResources.nonmatching()); 53 | } 54 | public static FingerprintImage probeGray() { 55 | return new FingerprintImage(332, 533, TestResources.probeGray()); 56 | } 57 | public static FingerprintImage matchingGray() { 58 | return new FingerprintImage(320, 407, TestResources.matchingGray()); 59 | } 60 | public static FingerprintImage nonmatchingGray() { 61 | return new FingerprintImage(333, 435, TestResources.nonmatchingGray()); 62 | } 63 | @Test 64 | public void decodeGray() { 65 | double score = new FingerprintMatcher(new FingerprintTemplate(probeGray())) 66 | .match(new FingerprintTemplate(matchingGray())); 67 | assertThat(score, greaterThan(40.0)); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/matcher/MatcherEngine.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.matcher; 3 | 4 | import com.machinezoo.sourceafis.engine.templates.*; 5 | import com.machinezoo.sourceafis.engine.transparency.*; 6 | 7 | public class MatcherEngine { 8 | public static double match(Probe probe, SearchTemplate candidate) { 9 | /* 10 | * Thread-local storage is fairly fast, but it's still a hash lookup, 11 | * so do not access TransparencySink.current() repeatedly in tight loops. 12 | */ 13 | var transparency = TransparencySink.current(); 14 | var thread = MatcherThread.current(); 15 | try { 16 | thread.pairing.reserveProbe(probe); 17 | thread.pairing.reserveCandidate(candidate); 18 | /* 19 | * Collection of support edges is very slow. It must be disabled on matcher level for it to have no performance impact. 20 | */ 21 | thread.pairing.supportEnabled = transparency.acceptsPairing(); 22 | RootEnumerator.enumerate(probe, candidate, thread.roots); 23 | // https://sourceafis.machinezoo.com/transparency/roots 24 | transparency.logRootPairs(thread.roots.count, thread.roots.pairs); 25 | double high = 0; 26 | int best = -1; 27 | for (int i = 0; i < thread.roots.count; ++i) { 28 | EdgeSpider.crawl(probe.template.edges, candidate.edges, thread.pairing, thread.roots.pairs[i], thread.queue); 29 | // https://sourceafis.machinezoo.com/transparency/pairing 30 | transparency.logPairing(thread.pairing); 31 | Scoring.compute(probe.template, candidate, thread.pairing, thread.score); 32 | // https://sourceafis.machinezoo.com/transparency/score 33 | transparency.logScore(thread.score); 34 | double partial = thread.score.shapedScore; 35 | if (best < 0 || partial > high) { 36 | high = partial; 37 | best = i; 38 | } 39 | thread.pairing.clear(); 40 | } 41 | if (best >= 0 && (transparency.acceptsBestPairing() || transparency.acceptsBestScore())) { 42 | thread.pairing.supportEnabled = transparency.acceptsBestPairing(); 43 | EdgeSpider.crawl(probe.template.edges, candidate.edges, thread.pairing, thread.roots.pairs[best], thread.queue); 44 | // https://sourceafis.machinezoo.com/transparency/pairing 45 | transparency.logBestPairing(thread.pairing); 46 | Scoring.compute(probe.template, candidate, thread.pairing, thread.score); 47 | // https://sourceafis.machinezoo.com/transparency/score 48 | transparency.logBestScore(thread.score); 49 | thread.pairing.clear(); 50 | } 51 | thread.roots.discard(); 52 | // https://sourceafis.machinezoo.com/transparency/best-match 53 | transparency.logBestMatch(best); 54 | return high; 55 | } catch (Throwable ex) { 56 | MatcherThread.kill(); 57 | throw ex; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/configuration/PlatformCheck.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.configuration; 3 | 4 | import java.io.*; 5 | import java.util.regex.*; 6 | import org.apache.commons.io.*; 7 | import com.machinezoo.noexception.*; 8 | import com.machinezoo.sourceafis.*; 9 | 10 | public class PlatformCheck { 11 | // https://stackoverflow.com/questions/2591083/getting-java-version-at-runtime 12 | private static final Pattern versionRe1 = Pattern.compile("1\\.([0-9]{1,3})\\..*"); 13 | private static final Pattern versionRe2 = Pattern.compile("([0-9]{1,3})\\..*"); 14 | private static void requireJava() { 15 | String version = System.getProperty("java.version"); 16 | /* 17 | * Property java.version should be always present, but let's guard against weird Java implementations. 18 | */ 19 | if (version != null) { 20 | Matcher matcher = versionRe1.matcher(version); 21 | if (!matcher.matches()) { 22 | matcher = versionRe2.matcher(version); 23 | /* 24 | * If no version pattern matches, we are running on Android or in some other weird JVM. 25 | * Since the version check does not work, we will just skip it. 26 | */ 27 | if (!matcher.matches()) 28 | return; 29 | } 30 | /* 31 | * Parsing will not throw, because we constrain the version to [0-9]{1,k} in the regex. 32 | */ 33 | int major = Integer.parseInt(matcher.group(1)); 34 | if (major < 11) 35 | throw new RuntimeException("SourceAFIS requires Java 11 or higher. Currently running JRE " + version + "."); 36 | } 37 | } 38 | /* 39 | * Eager checks should be executed automatically before lazy checks. 40 | */ 41 | static { 42 | requireJava(); 43 | } 44 | /* 45 | * Called to trigger eager checks above. Call to run() is placed in several places to ensure nothing runs before checks are complete. 46 | * Some code runs even before the static initializers that trigger call of this method. We cannot be 100% sure that platform check runs first. 47 | */ 48 | public static void run() { 49 | } 50 | public static byte[] resource(String filename) { 51 | return Exceptions.wrap(ex -> new IllegalStateException("Cannot read SourceAFIS resource: " + filename + ". Use proper dependency management tool.", ex)).get(() -> { 52 | try (InputStream stream = FingerprintTemplate.class.getResourceAsStream(filename)) { 53 | if (stream == null) 54 | throw new IllegalStateException("SourceAFIS resource not found: " + filename + ". Use proper dependency management tool."); 55 | return IOUtils.toByteArray(stream); 56 | } 57 | }); 58 | } 59 | public static boolean hasClass(String name) { 60 | try { 61 | Class.forName(name); 62 | return true; 63 | } catch (Throwable ex) { 64 | return false; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/templates/PersistentTemplate.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.templates; 3 | 4 | import java.util.*; 5 | import com.machinezoo.sourceafis.*; 6 | import com.machinezoo.sourceafis.engine.features.*; 7 | import com.machinezoo.sourceafis.engine.primitives.*; 8 | 9 | public class PersistentTemplate { 10 | public String version; 11 | public short width; 12 | public short height; 13 | public short[] positionsX; 14 | public short[] positionsY; 15 | public float[] directions; 16 | public String types; 17 | public PersistentTemplate() { 18 | } 19 | public PersistentTemplate(FeatureTemplate features) { 20 | version = FingerprintCompatibility.version() + "-java"; 21 | width = (short)features.size.x; 22 | height = (short)features.size.y; 23 | int count = features.minutiae.size(); 24 | positionsX = new short[count]; 25 | positionsY = new short[count]; 26 | directions = new float[count]; 27 | char[] chars = new char[count]; 28 | for (int i = 0; i < count; ++i) { 29 | var minutia = features.minutiae.get(i); 30 | positionsX[i] = (short)minutia.position.x; 31 | positionsY[i] = (short)minutia.position.y; 32 | directions[i] = minutia.direction; 33 | chars[i] = minutia.type == MinutiaType.BIFURCATION ? 'B' : 'E'; 34 | } 35 | types = new String(chars); 36 | } 37 | public FeatureTemplate mutable() { 38 | var minutiae = new ArrayList(); 39 | for (int i = 0; i < types.length(); ++i) { 40 | MinutiaType type = types.charAt(i) == 'B' ? MinutiaType.BIFURCATION : MinutiaType.ENDING; 41 | minutiae.add(new FeatureMinutia(new IntPoint(positionsX[i], positionsY[i]), directions[i], type)); 42 | } 43 | return new FeatureTemplate(new IntPoint(width, height), minutiae); 44 | } 45 | public void validate() { 46 | /* 47 | * Width and height are informative only. Don't validate them. Ditto for version string. 48 | */ 49 | Objects.requireNonNull(positionsX, "Null array of X positions."); 50 | Objects.requireNonNull(positionsY, "Null array of Y positions."); 51 | Objects.requireNonNull(directions, "Null array of minutia directions."); 52 | Objects.requireNonNull(types, "Null minutia type string."); 53 | if (positionsX.length != types.length() || positionsY.length != types.length() || directions.length != types.length()) 54 | throw new IllegalArgumentException("Inconsistent lengths of minutia property arrays."); 55 | for (int i = 0; i < types.length(); ++i) { 56 | if (Math.abs(positionsX[i]) > 10_000 || Math.abs(positionsY[i]) > 10_000) 57 | throw new IllegalArgumentException("Minutia position out of range."); 58 | if (!FloatAngle.normalized(directions[i])) 59 | throw new IllegalArgumentException("Denormalized minutia direction."); 60 | if (types.charAt(i) != 'E' && types.charAt(i) != 'B') 61 | throw new IllegalArgumentException("Unknown minutia type."); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/test/java/com/machinezoo/sourceafis/FingerprintCompatibilityTest.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis; 3 | 4 | import static org.hamcrest.MatcherAssert.*; 5 | import static org.hamcrest.Matchers.*; 6 | import org.junit.jupiter.api.*; 7 | import com.machinezoo.fingerprintio.*; 8 | 9 | public class FingerprintCompatibilityTest { 10 | @Test 11 | public void version() { 12 | assertThat(FingerprintCompatibility.version(), matchesPattern("^\\d+\\.\\d+\\.\\d+$")); 13 | } 14 | public static FingerprintTemplate probeIso() { 15 | return FingerprintCompatibility.importTemplate(TestResources.probeIso()); 16 | } 17 | public static FingerprintTemplate matchingIso() { 18 | return FingerprintCompatibility.importTemplate(TestResources.matchingIso()); 19 | } 20 | public static FingerprintTemplate nonmatchingIso() { 21 | return FingerprintCompatibility.importTemplate(TestResources.nonmatchingIso()); 22 | } 23 | private static class RoundtripTemplates { 24 | FingerprintTemplate extracted; 25 | FingerprintTemplate roundtripped; 26 | RoundtripTemplates(FingerprintTemplate extracted, TemplateFormat format) { 27 | this.extracted = extracted; 28 | roundtripped = FingerprintCompatibility.importTemplate(FingerprintCompatibility.exportTemplates(format, extracted)); 29 | } 30 | } 31 | private void match(RoundtripTemplates probe, RoundtripTemplates candidate, boolean matching) { 32 | match("native", probe.extracted, candidate.extracted, matching); 33 | match("roundtripped", probe.roundtripped, candidate.roundtripped, matching); 34 | match("mixed", probe.extracted, candidate.roundtripped, matching); 35 | } 36 | private void match(String kind, FingerprintTemplate probe, FingerprintTemplate candidate, boolean matching) { 37 | double score = new FingerprintMatcher(probe).match(candidate); 38 | if (matching) 39 | assertThat(kind, score, greaterThan(40.0)); 40 | else 41 | assertThat(kind, score, lessThan(20.0)); 42 | } 43 | private void roundtrip(TemplateFormat format) { 44 | RoundtripTemplates probe = new RoundtripTemplates(FingerprintTemplateTest.probe(), format); 45 | RoundtripTemplates matching = new RoundtripTemplates(FingerprintTemplateTest.matching(), format); 46 | RoundtripTemplates nonmatching = new RoundtripTemplates(FingerprintTemplateTest.nonmatching(), format); 47 | match(probe, matching, true); 48 | match(probe, nonmatching, false); 49 | } 50 | @Test 51 | public void roundtripAnsi378v2004() { 52 | roundtrip(TemplateFormat.ANSI_378_2004); 53 | } 54 | @Test 55 | public void roundtripAnsi378v2009() { 56 | roundtrip(TemplateFormat.ANSI_378_2009); 57 | } 58 | @Test 59 | public void roundtripAnsi378v2009AM1() { 60 | roundtrip(TemplateFormat.ANSI_378_2009_AM1); 61 | } 62 | @Test 63 | public void roundtripIso19794p2v2005() { 64 | roundtrip(TemplateFormat.ISO_19794_2_2005); 65 | } 66 | @Test 67 | public void roundtripIso19794p2v2011() { 68 | roundtrip(TemplateFormat.ISO_19794_2_2011); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/extractor/BinarizedImage.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.extractor; 3 | 4 | import com.machinezoo.sourceafis.engine.configuration.*; 5 | import com.machinezoo.sourceafis.engine.primitives.*; 6 | import com.machinezoo.sourceafis.engine.transparency.*; 7 | 8 | public class BinarizedImage { 9 | public static BooleanMatrix binarize(DoubleMatrix input, DoubleMatrix baseline, BooleanMatrix mask, BlockMap blocks) { 10 | IntPoint size = input.size(); 11 | BooleanMatrix binarized = new BooleanMatrix(size); 12 | for (IntPoint block : blocks.primary.blocks) 13 | if (mask.get(block)) { 14 | IntRect rect = blocks.primary.block(block); 15 | for (int y = rect.top(); y < rect.bottom(); ++y) 16 | for (int x = rect.left(); x < rect.right(); ++x) 17 | if (input.get(x, y) - baseline.get(x, y) > 0) 18 | binarized.set(x, y, true); 19 | } 20 | // https://sourceafis.machinezoo.com/transparency/binarized-image 21 | TransparencySink.current().log("binarized-image", binarized); 22 | return binarized; 23 | } 24 | private static void removeCrosses(BooleanMatrix input) { 25 | IntPoint size = input.size(); 26 | boolean any = true; 27 | while (any) { 28 | any = false; 29 | for (int y = 0; y < size.y - 1; ++y) 30 | for (int x = 0; x < size.x - 1; ++x) 31 | if (input.get(x, y) && input.get(x + 1, y + 1) && !input.get(x, y + 1) && !input.get(x + 1, y) 32 | || input.get(x, y + 1) && input.get(x + 1, y) && !input.get(x, y) && !input.get(x + 1, y + 1)) { 33 | input.set(x, y, false); 34 | input.set(x, y + 1, false); 35 | input.set(x + 1, y, false); 36 | input.set(x + 1, y + 1, false); 37 | any = true; 38 | } 39 | } 40 | } 41 | public static void cleanup(BooleanMatrix binary, BooleanMatrix mask) { 42 | IntPoint size = binary.size(); 43 | BooleanMatrix inverted = new BooleanMatrix(binary); 44 | inverted.invert(); 45 | BooleanMatrix islands = VoteFilter.vote(inverted, mask, Parameters.BINARIZED_VOTE_RADIUS, Parameters.BINARIZED_VOTE_MAJORITY, Parameters.BINARIZED_VOTE_BORDER_DISTANCE); 46 | BooleanMatrix holes = VoteFilter.vote(binary, mask, Parameters.BINARIZED_VOTE_RADIUS, Parameters.BINARIZED_VOTE_MAJORITY, Parameters.BINARIZED_VOTE_BORDER_DISTANCE); 47 | for (int y = 0; y < size.y; ++y) 48 | for (int x = 0; x < size.x; ++x) 49 | binary.set(x, y, binary.get(x, y) && !islands.get(x, y) || holes.get(x, y)); 50 | removeCrosses(binary); 51 | // https://sourceafis.machinezoo.com/transparency/filtered-binary-image 52 | TransparencySink.current().log("filtered-binary-image", binary); 53 | } 54 | public static BooleanMatrix invert(BooleanMatrix binary, BooleanMatrix mask) { 55 | IntPoint size = binary.size(); 56 | BooleanMatrix inverted = new BooleanMatrix(size); 57 | for (int y = 0; y < size.y; ++y) 58 | for (int x = 0; x < size.x; ++x) 59 | inverted.set(x, y, !binary.get(x, y) && mask.get(x, y)); 60 | return inverted; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/matcher/EdgeSpider.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.matcher; 3 | 4 | import java.util.*; 5 | import com.machinezoo.sourceafis.engine.configuration.*; 6 | import com.machinezoo.sourceafis.engine.features.*; 7 | import com.machinezoo.sourceafis.engine.primitives.*; 8 | 9 | public class EdgeSpider { 10 | private static final float COMPLEMENTARY_MAX_ANGLE_ERROR = FloatAngle.complementary(Parameters.MAX_ANGLE_ERROR); 11 | private static List matchPairs(NeighborEdge[] pstar, NeighborEdge[] cstar, MinutiaPairPool pool) { 12 | List results = new ArrayList<>(); 13 | int start = 0; 14 | int end = 0; 15 | for (int cindex = 0; cindex < cstar.length; ++cindex) { 16 | var cedge = cstar[cindex]; 17 | while (start < pstar.length && pstar[start].length < cedge.length - Parameters.MAX_DISTANCE_ERROR) 18 | ++start; 19 | if (end < start) 20 | end = start; 21 | while (end < pstar.length && pstar[end].length <= cedge.length + Parameters.MAX_DISTANCE_ERROR) 22 | ++end; 23 | for (int pindex = start; pindex < end; ++pindex) { 24 | var pedge = pstar[pindex]; 25 | float rdiff = FloatAngle.difference(pedge.referenceAngle, cedge.referenceAngle); 26 | if (rdiff <= Parameters.MAX_ANGLE_ERROR || rdiff >= COMPLEMENTARY_MAX_ANGLE_ERROR) { 27 | float ndiff = FloatAngle.difference(pedge.neighborAngle, cedge.neighborAngle); 28 | if (ndiff <= Parameters.MAX_ANGLE_ERROR || ndiff >= COMPLEMENTARY_MAX_ANGLE_ERROR) { 29 | MinutiaPair pair = pool.allocate(); 30 | pair.probe = pedge.neighbor; 31 | pair.candidate = cedge.neighbor; 32 | pair.distance = cedge.length; 33 | results.add(pair); 34 | } 35 | } 36 | } 37 | } 38 | return results; 39 | } 40 | private static void collectEdges(NeighborEdge[][] pedges, NeighborEdge[][] cedges, PairingGraph pairing, PriorityQueue queue) { 41 | var reference = pairing.tree[pairing.count - 1]; 42 | var pstar = pedges[reference.probe]; 43 | var cstar = cedges[reference.candidate]; 44 | for (var pair : matchPairs(pstar, cstar, pairing.pool)) { 45 | pair.probeRef = reference.probe; 46 | pair.candidateRef = reference.candidate; 47 | if (pairing.byCandidate[pair.candidate] == null && pairing.byProbe[pair.probe] == null) 48 | queue.add(pair); 49 | else 50 | pairing.support(pair); 51 | } 52 | } 53 | private static void skipPaired(PairingGraph pairing, PriorityQueue queue) { 54 | while (!queue.isEmpty() && (pairing.byProbe[queue.peek().probe] != null || pairing.byCandidate[queue.peek().candidate] != null)) 55 | pairing.support(queue.remove()); 56 | } 57 | public static void crawl(NeighborEdge[][] pedges, NeighborEdge[][] cedges, PairingGraph pairing, MinutiaPair root, PriorityQueue queue) { 58 | queue.add(root); 59 | do { 60 | pairing.addPair(queue.remove()); 61 | collectEdges(pedges, cedges, pairing, queue); 62 | skipPaired(pairing, queue); 63 | } while (!queue.isEmpty()); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/primitives/IntRect.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.primitives; 3 | 4 | import java.util.*; 5 | 6 | public class IntRect implements Iterable { 7 | public final int x; 8 | public final int y; 9 | public final int width; 10 | public final int height; 11 | public IntRect(int x, int y, int width, int height) { 12 | this.x = x; 13 | this.y = y; 14 | this.width = width; 15 | this.height = height; 16 | } 17 | public IntRect(IntPoint size) { 18 | this(0, 0, size.x, size.y); 19 | } 20 | public int left() { 21 | return x; 22 | } 23 | public int top() { 24 | return y; 25 | } 26 | public int right() { 27 | return x + width; 28 | } 29 | public int bottom() { 30 | return y + height; 31 | } 32 | public int area() { 33 | return width * height; 34 | } 35 | public static IntRect between(int startX, int startY, int endX, int endY) { 36 | return new IntRect(startX, startY, endX - startX, endY - startY); 37 | } 38 | public static IntRect between(IntPoint start, IntPoint end) { 39 | return between(start.x, start.y, end.x, end.y); 40 | } 41 | public static IntRect around(int x, int y, int radius) { 42 | return between(x - radius, y - radius, x + radius + 1, y + radius + 1); 43 | } 44 | public static IntRect around(IntPoint center, int radius) { 45 | return around(center.x, center.y, radius); 46 | } 47 | public IntPoint center() { 48 | return new IntPoint((right() + left()) / 2, (top() + bottom()) / 2); 49 | } 50 | public IntRect intersect(IntRect other) { 51 | return between( 52 | new IntPoint(Math.max(left(), other.left()), Math.max(top(), other.top())), 53 | new IntPoint(Math.min(right(), other.right()), Math.min(bottom(), other.bottom()))); 54 | } 55 | public IntRect move(IntPoint delta) { 56 | return new IntRect(x + delta.x, y + delta.y, width, height); 57 | } 58 | private List fields() { 59 | return Arrays.asList(x, y, width, height); 60 | } 61 | @Override 62 | public boolean equals(Object obj) { 63 | return obj instanceof IntRect && fields().equals(((IntRect)obj).fields()); 64 | } 65 | @Override 66 | public int hashCode() { 67 | return Objects.hash(x, y, width, height); 68 | } 69 | @Override 70 | public String toString() { 71 | return String.format("[%d,%d] @ [%d,%d]", width, height, x, y); 72 | } 73 | @Override 74 | public Iterator iterator() { 75 | return new BlockIterator(); 76 | } 77 | private class BlockIterator implements Iterator { 78 | int atX; 79 | int atY; 80 | @Override 81 | public boolean hasNext() { 82 | return atY < height && atX < width; 83 | } 84 | @Override 85 | public IntPoint next() { 86 | if (!hasNext()) 87 | throw new NoSuchElementException(); 88 | IntPoint result = new IntPoint(x + atX, y + atY); 89 | ++atX; 90 | if (atX >= width) { 91 | atX = 0; 92 | ++atY; 93 | } 94 | return result; 95 | } 96 | @Override 97 | public void remove() { 98 | throw new UnsupportedOperationException(); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/extractor/FeatureExtractor.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.extractor; 3 | 4 | import com.machinezoo.sourceafis.engine.configuration.*; 5 | import com.machinezoo.sourceafis.engine.extractor.minutiae.*; 6 | import com.machinezoo.sourceafis.engine.extractor.skeletons.*; 7 | import com.machinezoo.sourceafis.engine.features.*; 8 | import com.machinezoo.sourceafis.engine.primitives.*; 9 | import com.machinezoo.sourceafis.engine.templates.*; 10 | import com.machinezoo.sourceafis.engine.transparency.*; 11 | 12 | public class FeatureExtractor { 13 | public static FeatureTemplate extract(DoubleMatrix raw, double dpi) { 14 | // https://sourceafis.machinezoo.com/transparency/decoded-image 15 | TransparencySink.current().log("decoded-image", raw); 16 | raw = ImageResizer.resize(raw, dpi); 17 | // https://sourceafis.machinezoo.com/transparency/scaled-image 18 | TransparencySink.current().log("scaled-image", raw); 19 | BlockMap blocks = new BlockMap(raw.width, raw.height, Parameters.BLOCK_SIZE); 20 | // https://sourceafis.machinezoo.com/transparency/blocks 21 | TransparencySink.current().log("blocks", blocks); 22 | HistogramCube histogram = LocalHistograms.create(blocks, raw); 23 | HistogramCube smoothHistogram = LocalHistograms.smooth(blocks, histogram); 24 | BooleanMatrix mask = SegmentationMask.compute(blocks, histogram); 25 | DoubleMatrix equalized = ImageEqualization.equalize(blocks, raw, smoothHistogram, mask); 26 | DoubleMatrix orientation = BlockOrientations.compute(equalized, mask, blocks); 27 | DoubleMatrix smoothed = OrientedSmoothing.parallel(equalized, orientation, mask, blocks); 28 | DoubleMatrix orthogonal = OrientedSmoothing.orthogonal(smoothed, orientation, mask, blocks); 29 | BooleanMatrix binary = BinarizedImage.binarize(smoothed, orthogonal, mask, blocks); 30 | BooleanMatrix pixelMask = SegmentationMask.pixelwise(mask, blocks); 31 | BinarizedImage.cleanup(binary, pixelMask); 32 | BooleanMatrix inverted = BinarizedImage.invert(binary, pixelMask); 33 | BooleanMatrix innerMask = SegmentationMask.inner(pixelMask); 34 | Skeleton ridges = Skeletons.create(binary, SkeletonType.RIDGES); 35 | Skeleton valleys = Skeletons.create(inverted, SkeletonType.VALLEYS); 36 | var template = new FeatureTemplate(raw.size(), MinutiaCollector.collect(ridges, valleys)); 37 | // https://sourceafis.machinezoo.com/transparency/skeleton-minutiae 38 | TransparencySink.current().log("skeleton-minutiae", template); 39 | InnerMinutiaeFilter.apply(template.minutiae, innerMask); 40 | // https://sourceafis.machinezoo.com/transparency/inner-minutiae 41 | TransparencySink.current().log("inner-minutiae", template); 42 | MinutiaCloudFilter.apply(template.minutiae); 43 | // https://sourceafis.machinezoo.com/transparency/removed-minutia-clouds 44 | TransparencySink.current().log("removed-minutia-clouds", template); 45 | template = new FeatureTemplate(template.size, TopMinutiaeFilter.apply(template.minutiae)); 46 | // https://sourceafis.machinezoo.com/transparency/top-minutiae 47 | TransparencySink.current().log("top-minutiae", template); 48 | return template; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/extractor/SegmentationMask.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.extractor; 3 | 4 | import com.machinezoo.sourceafis.engine.configuration.*; 5 | import com.machinezoo.sourceafis.engine.primitives.*; 6 | import com.machinezoo.sourceafis.engine.transparency.*; 7 | 8 | public class SegmentationMask { 9 | private static BooleanMatrix filter(BooleanMatrix input) { 10 | return VoteFilter.vote(input, null, Parameters.BLOCK_ERRORS_VOTE_RADIUS, Parameters.BLOCK_ERRORS_VOTE_MAJORITY, Parameters.BLOCK_ERRORS_VOTE_BORDER_DISTANCE); 11 | } 12 | public static BooleanMatrix compute(BlockMap blocks, HistogramCube histogram) { 13 | DoubleMatrix contrast = ClippedContrast.compute(blocks, histogram); 14 | BooleanMatrix mask = AbsoluteContrastMask.compute(contrast); 15 | mask.merge(RelativeContrastMask.compute(contrast, blocks)); 16 | // https://sourceafis.machinezoo.com/transparency/combined-mask 17 | TransparencySink.current().log("combined-mask", mask); 18 | mask.merge(filter(mask)); 19 | mask.invert(); 20 | mask.merge(filter(mask)); 21 | mask.merge(filter(mask)); 22 | mask.merge(VoteFilter.vote(mask, null, Parameters.MASK_VOTE_RADIUS, Parameters.MASK_VOTE_MAJORITY, Parameters.MASK_VOTE_BORDER_DISTANCE)); 23 | // https://sourceafis.machinezoo.com/transparency/filtered-mask 24 | TransparencySink.current().log("filtered-mask", mask); 25 | return mask; 26 | } 27 | public static BooleanMatrix pixelwise(BooleanMatrix mask, BlockMap blocks) { 28 | BooleanMatrix pixelized = new BooleanMatrix(blocks.pixels); 29 | for (IntPoint block : blocks.primary.blocks) 30 | if (mask.get(block)) 31 | for (IntPoint pixel : blocks.primary.block(block)) 32 | pixelized.set(pixel, true); 33 | // https://sourceafis.machinezoo.com/transparency/pixel-mask 34 | TransparencySink.current().log("pixel-mask", pixelized); 35 | return pixelized; 36 | } 37 | private static BooleanMatrix shrink(BooleanMatrix mask, int amount) { 38 | IntPoint size = mask.size(); 39 | BooleanMatrix shrunk = new BooleanMatrix(size); 40 | for (int y = amount; y < size.y - amount; ++y) 41 | for (int x = amount; x < size.x - amount; ++x) 42 | shrunk.set(x, y, mask.get(x, y - amount) && mask.get(x, y + amount) && mask.get(x - amount, y) && mask.get(x + amount, y)); 43 | return shrunk; 44 | } 45 | public static BooleanMatrix inner(BooleanMatrix outer) { 46 | IntPoint size = outer.size(); 47 | BooleanMatrix inner = new BooleanMatrix(size); 48 | for (int y = 1; y < size.y - 1; ++y) 49 | for (int x = 1; x < size.x - 1; ++x) 50 | inner.set(x, y, outer.get(x, y)); 51 | if (Parameters.INNER_MASK_BORDER_DISTANCE >= 1) 52 | inner = shrink(inner, 1); 53 | int total = 1; 54 | for (int step = 1; total + step <= Parameters.INNER_MASK_BORDER_DISTANCE; step *= 2) { 55 | inner = shrink(inner, step); 56 | total += step; 57 | } 58 | if (total < Parameters.INNER_MASK_BORDER_DISTANCE) 59 | inner = shrink(inner, Parameters.INNER_MASK_BORDER_DISTANCE - total); 60 | // https://sourceafis.machinezoo.com/transparency/inner-mask 61 | TransparencySink.current().log("inner-mask", inner); 62 | return inner; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/images/ImageDecoder.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.images; 3 | 4 | import static java.util.stream.Collectors.*; 5 | import java.util.*; 6 | 7 | /* 8 | * We cannot just use ImageIO, because fingerprints often come in formats not supported by ImageIO. 9 | * We would also like SourceAFIS to work out of the box on Android, which doesn't have ImageIO at all. 10 | * For these reasons, we have several image decoders that are tried in order for every image. 11 | * 12 | * This should really be a separate image decoding library, but AFAIK there is no such universal library. 13 | * Perhaps one should be created by forking this code off SourceAFIS and expanding it considerably. 14 | * Such library can be generalized to suit many applications by making supported formats 15 | * configurable via maven's provided dependencies. 16 | */ 17 | public abstract class ImageDecoder { 18 | /* 19 | * This is used to check whether the image decoder implementation exists. 20 | * If it does not, we can produce understandable error message instead of ClassNotFoundException. 21 | */ 22 | public abstract boolean available(); 23 | public abstract String name(); 24 | /* 25 | * Decoding method never returns null. It throws if it fails to decode the template, 26 | * including cases when the decoder simply doesn't support the image format. 27 | */ 28 | public abstract DecodedImage decode(byte[] image); 29 | /* 30 | * Order is significant. If multiple decoders support the format, the first one wins. 31 | * This list is ordered to favor more common image formats and more common image decoders. 32 | * This makes sure that SourceAFIS performs equally well for common formats and common decoders 33 | * regardless of how many special-purpose decoders are added to this list. 34 | */ 35 | private static final List ALL = Arrays.asList( 36 | new ImageIODecoder(), 37 | new WsqDecoder(), 38 | new AndroidImageDecoder()); 39 | public static DecodedImage decodeAny(byte[] image) { 40 | Map exceptions = new HashMap<>(); 41 | for (ImageDecoder decoder : ALL) { 42 | try { 43 | if (!decoder.available()) 44 | throw new UnsupportedOperationException("Image decoder is not available."); 45 | return decoder.decode(image); 46 | } catch (Throwable ex) { 47 | exceptions.put(decoder, ex); 48 | } 49 | } 50 | /* 51 | * We should create an exception type that contains a lists of exceptions from all decoders. 52 | * But for now we don't want to complicate SourceAFIS API. 53 | * It will wait until this code gets moved to a separate image decoding library. 54 | * For now, we just summarize all the exceptions in a long message. 55 | */ 56 | throw new IllegalArgumentException(String.format("Unsupported image format [%s].", ALL.stream() 57 | .map(d -> String.format("%s = '%s'", d.name(), formatError(exceptions.get(d)))) 58 | .collect(joining(", ")))); 59 | } 60 | private static String formatError(Throwable exception) { 61 | List ancestors = new ArrayList<>(); 62 | for (Throwable ancestor = exception; ancestor != null; ancestor = ancestor.getCause()) 63 | ancestors.add(ancestor); 64 | return ancestors.stream() 65 | .map(ex -> ex.toString()) 66 | .collect(joining(" -> ")); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/extractor/OrientedSmoothing.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.extractor; 3 | 4 | import java.util.*; 5 | import com.machinezoo.sourceafis.engine.configuration.*; 6 | import com.machinezoo.sourceafis.engine.primitives.*; 7 | import com.machinezoo.sourceafis.engine.transparency.*; 8 | 9 | public class OrientedSmoothing { 10 | private static IntPoint[][] lines(int resolution, int radius, double step) { 11 | IntPoint[][] result = new IntPoint[resolution][]; 12 | for (int orientationIndex = 0; orientationIndex < resolution; ++orientationIndex) { 13 | List line = new ArrayList<>(); 14 | line.add(IntPoint.ZERO); 15 | DoublePoint direction = DoubleAngle.toVector(DoubleAngle.fromOrientation(DoubleAngle.bucketCenter(orientationIndex, resolution))); 16 | for (double r = radius; r >= 0.5; r /= step) { 17 | IntPoint sample = direction.multiply(r).round(); 18 | if (!line.contains(sample)) { 19 | line.add(sample); 20 | line.add(sample.negate()); 21 | } 22 | } 23 | result[orientationIndex] = line.toArray(new IntPoint[line.size()]); 24 | } 25 | return result; 26 | } 27 | private static DoubleMatrix smooth(DoubleMatrix input, DoubleMatrix orientation, BooleanMatrix mask, BlockMap blocks, double angle, IntPoint[][] lines) { 28 | DoubleMatrix output = new DoubleMatrix(input.size()); 29 | for (IntPoint block : blocks.primary.blocks) { 30 | if (mask.get(block)) { 31 | IntPoint[] line = lines[DoubleAngle.quantize(DoubleAngle.add(orientation.get(block), angle), lines.length)]; 32 | for (IntPoint linePoint : line) { 33 | IntRect target = blocks.primary.block(block); 34 | IntRect source = target.move(linePoint).intersect(new IntRect(blocks.pixels)); 35 | target = source.move(linePoint.negate()); 36 | for (int y = target.top(); y < target.bottom(); ++y) 37 | for (int x = target.left(); x < target.right(); ++x) 38 | output.add(x, y, input.get(x + linePoint.x, y + linePoint.y)); 39 | } 40 | IntRect blockArea = blocks.primary.block(block); 41 | for (int y = blockArea.top(); y < blockArea.bottom(); ++y) 42 | for (int x = blockArea.left(); x < blockArea.right(); ++x) 43 | output.multiply(x, y, 1.0 / line.length); 44 | } 45 | } 46 | return output; 47 | } 48 | public static DoubleMatrix parallel(DoubleMatrix input, DoubleMatrix orientation, BooleanMatrix mask, BlockMap blocks) { 49 | var lines = lines(Parameters.PARALLEL_SMOOTHING_RESOLUTION, Parameters.PARALLEL_SMOOTHING_RADIUS, Parameters.PARALLEL_SMOOTHING_STEP); 50 | var smoothed = smooth(input, orientation, mask, blocks, 0, lines); 51 | // https://sourceafis.machinezoo.com/transparency/parallel-smoothing 52 | TransparencySink.current().log("parallel-smoothing", smoothed); 53 | return smoothed; 54 | } 55 | public static DoubleMatrix orthogonal(DoubleMatrix input, DoubleMatrix orientation, BooleanMatrix mask, BlockMap blocks) { 56 | var lines = lines(Parameters.ORTHOGONAL_SMOOTHING_RESOLUTION, Parameters.ORTHOGONAL_SMOOTHING_RADIUS, Parameters.ORTHOGONAL_SMOOTHING_STEP); 57 | var smoothed = smooth(input, orientation, mask, blocks, Math.PI, lines); 58 | // https://sourceafis.machinezoo.com/transparency/orthogonal-smoothing 59 | TransparencySink.current().log("orthogonal-smoothing", smoothed); 60 | return smoothed; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/test/java/com/machinezoo/sourceafis/engine/primitives/IntRectTest.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.primitives; 3 | 4 | import static org.junit.jupiter.api.Assertions.*; 5 | import java.util.*; 6 | import org.junit.jupiter.api.*; 7 | 8 | public class IntRectTest { 9 | @Test 10 | public void constructor() { 11 | IntRect b = new IntRect(2, 3, 10, 20); 12 | assertEquals(2, b.x); 13 | assertEquals(3, b.y); 14 | assertEquals(10, b.width); 15 | assertEquals(20, b.height); 16 | } 17 | @Test 18 | public void constructorFromPoint() { 19 | IntRect b = new IntRect(new IntPoint(2, 3)); 20 | assertEquals(0, b.x); 21 | assertEquals(0, b.y); 22 | assertEquals(2, b.width); 23 | assertEquals(3, b.height); 24 | } 25 | @Test 26 | public void left() { 27 | assertEquals(2, new IntRect(2, 3, 4, 5).left()); 28 | } 29 | @Test 30 | public void right() { 31 | assertEquals(6, new IntRect(2, 3, 4, 5).right()); 32 | } 33 | @Test 34 | public void bottom() { 35 | assertEquals(3, new IntRect(2, 3, 4, 5).top()); 36 | } 37 | @Test 38 | public void top() { 39 | assertEquals(8, new IntRect(2, 3, 4, 5).bottom()); 40 | } 41 | @Test 42 | public void area() { 43 | assertEquals(20, new IntRect(2, 3, 4, 5).area()); 44 | } 45 | @Test 46 | public void betweenCoordinates() { 47 | assertEquals(new IntRect(2, 3, 4, 5), IntRect.between(2, 3, 6, 8)); 48 | } 49 | @Test 50 | public void betweenPoints() { 51 | assertEquals(new IntRect(2, 3, 4, 5), IntRect.between(new IntPoint(2, 3), new IntPoint(6, 8))); 52 | } 53 | @Test 54 | public void aroundCoordinates() { 55 | assertEquals(new IntRect(2, 3, 5, 5), IntRect.around(4, 5, 2)); 56 | } 57 | @Test 58 | public void aroundPoint() { 59 | assertEquals(new IntRect(2, 3, 5, 5), IntRect.around(new IntPoint(4, 5), 2)); 60 | } 61 | @Test 62 | public void center() { 63 | assertEquals(new IntPoint(4, 5), new IntRect(2, 3, 4, 4).center()); 64 | assertEquals(new IntPoint(4, 5), new IntRect(2, 3, 5, 5).center()); 65 | assertEquals(new IntPoint(2, 3), new IntRect(2, 3, 0, 0).center()); 66 | } 67 | @Test 68 | public void move() { 69 | assertEquals(new IntRect(12, 23, 4, 5), new IntRect(2, 3, 4, 5).move(new IntPoint(10, 20))); 70 | } 71 | @Test 72 | public void intersect() { 73 | assertEquals(new IntRect(58, 30, 2, 5), new IntRect(20, 30, 40, 50).intersect(new IntRect(58, 27, 7, 8))); 74 | assertEquals(new IntRect(20, 77, 5, 3), new IntRect(20, 30, 40, 50).intersect(new IntRect(18, 77, 7, 8))); 75 | assertEquals(new IntRect(30, 40, 20, 30), new IntRect(20, 30, 40, 50).intersect(new IntRect(30, 40, 20, 30))); 76 | } 77 | @Test 78 | public void iterator() { 79 | List l = new ArrayList<>(); 80 | for (IntPoint c : new IntRect(4, 5, 2, 3)) 81 | l.add(c); 82 | assertEquals(Arrays.asList(new IntPoint(4, 5), new IntPoint(5, 5), new IntPoint(4, 6), new IntPoint(5, 6), new IntPoint(4, 7), new IntPoint(5, 7)), l); 83 | for (IntPoint c : new IntRect(2, 3, 0, 3)) 84 | fail(c.toString()); 85 | for (IntPoint c : new IntRect(2, 3, 3, 0)) 86 | fail(c.toString()); 87 | for (IntPoint c : new IntRect(2, 3, -1, 3)) 88 | fail(c.toString()); 89 | for (IntPoint c : new IntRect(2, 3, 3, -1)) 90 | fail(c.toString()); 91 | } 92 | @Test 93 | public void toString_readable() { 94 | assertEquals("[10,20] @ [2,3]", new IntRect(2, 3, 10, 20).toString()); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/extractor/ImageEqualization.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.extractor; 3 | 4 | import java.util.*; 5 | import com.machinezoo.sourceafis.engine.configuration.*; 6 | import com.machinezoo.sourceafis.engine.primitives.*; 7 | import com.machinezoo.sourceafis.engine.transparency.*; 8 | 9 | public class ImageEqualization { 10 | public static DoubleMatrix equalize(BlockMap blocks, DoubleMatrix image, HistogramCube histogram, BooleanMatrix blockMask) { 11 | final double rangeMin = -1; 12 | final double rangeMax = 1; 13 | final double rangeSize = rangeMax - rangeMin; 14 | final double widthMax = rangeSize / 256 * Parameters.MAX_EQUALIZATION_SCALING; 15 | final double widthMin = rangeSize / 256 * Parameters.MIN_EQUALIZATION_SCALING; 16 | double[] limitedMin = new double[histogram.bins]; 17 | double[] limitedMax = new double[histogram.bins]; 18 | double[] dequantized = new double[histogram.bins]; 19 | for (int i = 0; i < histogram.bins; ++i) { 20 | limitedMin[i] = Math.max(i * widthMin + rangeMin, rangeMax - (histogram.bins - 1 - i) * widthMax); 21 | limitedMax[i] = Math.min(i * widthMax + rangeMin, rangeMax - (histogram.bins - 1 - i) * widthMin); 22 | dequantized[i] = i / (double)(histogram.bins - 1); 23 | } 24 | Map mappings = new HashMap<>(); 25 | for (IntPoint corner : blocks.secondary.blocks) { 26 | double[] mapping = new double[histogram.bins]; 27 | mappings.put(corner, mapping); 28 | if (blockMask.get(corner, false) || blockMask.get(corner.x - 1, corner.y, false) 29 | || blockMask.get(corner.x, corner.y - 1, false) || blockMask.get(corner.x - 1, corner.y - 1, false)) { 30 | double step = rangeSize / histogram.sum(corner); 31 | double top = rangeMin; 32 | for (int i = 0; i < histogram.bins; ++i) { 33 | double band = histogram.get(corner, i) * step; 34 | double equalized = top + dequantized[i] * band; 35 | top += band; 36 | if (equalized < limitedMin[i]) 37 | equalized = limitedMin[i]; 38 | if (equalized > limitedMax[i]) 39 | equalized = limitedMax[i]; 40 | mapping[i] = equalized; 41 | } 42 | } 43 | } 44 | DoubleMatrix result = new DoubleMatrix(blocks.pixels); 45 | for (IntPoint block : blocks.primary.blocks) { 46 | IntRect area = blocks.primary.block(block); 47 | if (blockMask.get(block)) { 48 | double[] topleft = mappings.get(block); 49 | double[] topright = mappings.get(new IntPoint(block.x + 1, block.y)); 50 | double[] bottomleft = mappings.get(new IntPoint(block.x, block.y + 1)); 51 | double[] bottomright = mappings.get(new IntPoint(block.x + 1, block.y + 1)); 52 | for (int y = area.top(); y < area.bottom(); ++y) 53 | for (int x = area.left(); x < area.right(); ++x) { 54 | int depth = histogram.constrain((int)(image.get(x, y) * histogram.bins)); 55 | double rx = (x - area.x + 0.5) / area.width; 56 | double ry = (y - area.y + 0.5) / area.height; 57 | result.set(x, y, Doubles.interpolate(bottomleft[depth], bottomright[depth], topleft[depth], topright[depth], rx, ry)); 58 | } 59 | } else { 60 | for (int y = area.top(); y < area.bottom(); ++y) 61 | for (int x = area.left(); x < area.right(); ++x) 62 | result.set(x, y, -1); 63 | } 64 | } 65 | // https://sourceafis.machinezoo.com/transparency/equalized-image 66 | TransparencySink.current().log("equalized-image", result); 67 | return result; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/extractor/skeletons/SkeletonGapFilter.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.extractor.skeletons; 3 | 4 | import java.util.*; 5 | import com.machinezoo.sourceafis.engine.configuration.*; 6 | import com.machinezoo.sourceafis.engine.features.*; 7 | import com.machinezoo.sourceafis.engine.primitives.*; 8 | import com.machinezoo.sourceafis.engine.transparency.*; 9 | 10 | public class SkeletonGapFilter { 11 | private static void addGapRidge(BooleanMatrix shadow, SkeletonGap gap, IntPoint[] line) { 12 | SkeletonRidge ridge = new SkeletonRidge(); 13 | for (IntPoint point : line) 14 | ridge.points.add(point); 15 | ridge.start(gap.end1); 16 | ridge.end(gap.end2); 17 | for (IntPoint point : line) 18 | shadow.set(point, true); 19 | } 20 | private static boolean isRidgeOverlapping(IntPoint[] line, BooleanMatrix shadow) { 21 | for (int i = Parameters.TOLERATED_GAP_OVERLAP; i < line.length - Parameters.TOLERATED_GAP_OVERLAP; ++i) 22 | if (shadow.get(line[i])) 23 | return true; 24 | return false; 25 | } 26 | private static IntPoint angleSampleForGapRemoval(SkeletonMinutia minutia) { 27 | SkeletonRidge ridge = minutia.ridges.get(0); 28 | if (Parameters.GAP_ANGLE_OFFSET < ridge.points.size()) 29 | return ridge.points.get(Parameters.GAP_ANGLE_OFFSET); 30 | else 31 | return ridge.end().position; 32 | } 33 | private static boolean isWithinGapLimits(SkeletonMinutia end1, SkeletonMinutia end2) { 34 | int distanceSq = end1.position.minus(end2.position).lengthSq(); 35 | if (distanceSq <= Integers.sq(Parameters.MAX_RUPTURE_SIZE)) 36 | return true; 37 | if (distanceSq > Integers.sq(Parameters.MAX_GAP_SIZE)) 38 | return false; 39 | double gapDirection = DoubleAngle.atan(end1.position, end2.position); 40 | double direction1 = DoubleAngle.atan(end1.position, angleSampleForGapRemoval(end1)); 41 | if (DoubleAngle.distance(direction1, DoubleAngle.opposite(gapDirection)) > Parameters.MAX_GAP_ANGLE) 42 | return false; 43 | double direction2 = DoubleAngle.atan(end2.position, angleSampleForGapRemoval(end2)); 44 | if (DoubleAngle.distance(direction2, gapDirection) > Parameters.MAX_GAP_ANGLE) 45 | return false; 46 | return true; 47 | } 48 | public static void apply(Skeleton skeleton) { 49 | PriorityQueue queue = new PriorityQueue<>(); 50 | for (SkeletonMinutia end1 : skeleton.minutiae) 51 | if (end1.ridges.size() == 1 && end1.ridges.get(0).points.size() >= Parameters.SHORTEST_JOINED_ENDING) 52 | for (SkeletonMinutia end2 : skeleton.minutiae) 53 | if (end2 != end1 && end2.ridges.size() == 1 && end1.ridges.get(0).end() != end2 54 | && end2.ridges.get(0).points.size() >= Parameters.SHORTEST_JOINED_ENDING && isWithinGapLimits(end1, end2)) { 55 | SkeletonGap gap = new SkeletonGap(); 56 | gap.distance = end1.position.minus(end2.position).lengthSq(); 57 | gap.end1 = end1; 58 | gap.end2 = end2; 59 | queue.add(gap); 60 | } 61 | BooleanMatrix shadow = skeleton.shadow(); 62 | while (!queue.isEmpty()) { 63 | SkeletonGap gap = queue.remove(); 64 | if (gap.end1.ridges.size() == 1 && gap.end2.ridges.size() == 1) { 65 | IntPoint[] line = gap.end1.position.lineTo(gap.end2.position); 66 | if (!isRidgeOverlapping(line, shadow)) 67 | addGapRidge(shadow, gap, line); 68 | } 69 | } 70 | SkeletonKnotFilter.apply(skeleton); 71 | // https://sourceafis.machinezoo.com/transparency/removed-gaps 72 | TransparencySink.current().logSkeleton("removed-gaps", skeleton); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/templates/Ansi378v2009Codec.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.templates; 3 | 4 | import static java.util.stream.Collectors.*; 5 | import java.util.*; 6 | import java.util.stream.*; 7 | import com.machinezoo.fingerprintio.ansi378v2009.*; 8 | import com.machinezoo.noexception.*; 9 | import com.machinezoo.sourceafis.engine.features.*; 10 | import com.machinezoo.sourceafis.engine.primitives.*; 11 | 12 | class Ansi378v2009Codec extends TemplateCodec { 13 | @Override 14 | public byte[] encode(List templates) { 15 | Ansi378v2009Template iotemplate = new Ansi378v2009Template(); 16 | iotemplate.fingerprints = IntStream.range(0, templates.size()) 17 | .mapToObj(n -> encode(n, templates.get(n))) 18 | .collect(toList()); 19 | return iotemplate.toByteArray(); 20 | } 21 | @Override 22 | public List decode(byte[] serialized, ExceptionHandler handler) { 23 | return new Ansi378v2009Template(serialized, handler).fingerprints.stream() 24 | .map(fp -> decode(fp)) 25 | .collect(toList()); 26 | } 27 | private static Ansi378v2009Fingerprint encode(int offset, FeatureTemplate template) { 28 | int resolution = (int)Math.round(500 / 2.54); 29 | Ansi378v2009Fingerprint iofingerprint = new Ansi378v2009Fingerprint(); 30 | iofingerprint.view = offset; 31 | iofingerprint.width = template.size.x; 32 | iofingerprint.height = template.size.y; 33 | iofingerprint.resolutionX = resolution; 34 | iofingerprint.resolutionY = resolution; 35 | iofingerprint.minutiae = template.minutiae.stream() 36 | .map(m -> encode(m)) 37 | .collect(toList()); 38 | return iofingerprint; 39 | } 40 | private static FeatureTemplate decode(Ansi378v2009Fingerprint iofingerprint) { 41 | TemplateResolution resolution = new TemplateResolution(); 42 | resolution.dpiX = iofingerprint.resolutionX * 2.54; 43 | resolution.dpiY = iofingerprint.resolutionY * 2.54; 44 | return new FeatureTemplate( 45 | resolution.decode(iofingerprint.width, iofingerprint.height), 46 | iofingerprint.minutiae.stream() 47 | .map(m -> decode(m, resolution)) 48 | .collect(toList())); 49 | } 50 | private static Ansi378v2009Minutia encode(FeatureMinutia minutia) { 51 | Ansi378v2009Minutia iominutia = new Ansi378v2009Minutia(); 52 | iominutia.positionX = minutia.position.x; 53 | iominutia.positionY = minutia.position.y; 54 | iominutia.angle = encodeAngle(minutia.direction); 55 | iominutia.type = encode(minutia.type); 56 | return iominutia; 57 | } 58 | private static FeatureMinutia decode(Ansi378v2009Minutia iominutia, TemplateResolution resolution) { 59 | return new FeatureMinutia( 60 | resolution.decode(iominutia.positionX, iominutia.positionY), 61 | decodeAngle(iominutia.angle), 62 | decode(iominutia.type)); 63 | } 64 | private static int encodeAngle(float angle) { 65 | return (int)Math.ceil(DoubleAngle.complementary(angle) * DoubleAngle.INV_PI2 * 360 / 2) % 180; 66 | } 67 | private static float decodeAngle(int ioangle) { 68 | return FloatAngle.complementary(((2 * ioangle - 1 + 360) % 360) / 360.0f * FloatAngle.PI2); 69 | } 70 | private static Ansi378v2009MinutiaType encode(MinutiaType type) { 71 | switch (type) { 72 | case ENDING: 73 | return Ansi378v2009MinutiaType.ENDING; 74 | case BIFURCATION: 75 | return Ansi378v2009MinutiaType.BIFURCATION; 76 | default : 77 | return Ansi378v2009MinutiaType.ENDING; 78 | } 79 | } 80 | private static MinutiaType decode(Ansi378v2009MinutiaType iotype) { 81 | switch (iotype) { 82 | case ENDING: 83 | return MinutiaType.ENDING; 84 | case BIFURCATION: 85 | return MinutiaType.BIFURCATION; 86 | default : 87 | return MinutiaType.ENDING; 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/templates/Ansi378v2009Am1Codec.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.templates; 3 | 4 | import static java.util.stream.Collectors.*; 5 | import java.util.*; 6 | import java.util.stream.*; 7 | import com.machinezoo.fingerprintio.ansi378v2009am1.*; 8 | import com.machinezoo.noexception.*; 9 | import com.machinezoo.sourceafis.engine.features.*; 10 | import com.machinezoo.sourceafis.engine.primitives.*; 11 | 12 | class Ansi378v2009Am1Codec extends TemplateCodec { 13 | @Override 14 | public byte[] encode(List templates) { 15 | Ansi378v2009Am1Template iotemplate = new Ansi378v2009Am1Template(); 16 | iotemplate.fingerprints = IntStream.range(0, templates.size()) 17 | .mapToObj(n -> encode(n, templates.get(n))) 18 | .collect(toList()); 19 | return iotemplate.toByteArray(); 20 | } 21 | @Override 22 | public List decode(byte[] serialized, ExceptionHandler handler) { 23 | return new Ansi378v2009Am1Template(serialized, handler).fingerprints.stream() 24 | .map(fp -> decode(fp)) 25 | .collect(toList()); 26 | } 27 | private static Ansi378v2009Am1Fingerprint encode(int offset, FeatureTemplate template) { 28 | int resolution = (int)Math.round(500 / 2.54); 29 | Ansi378v2009Am1Fingerprint iofingerprint = new Ansi378v2009Am1Fingerprint(); 30 | iofingerprint.view = offset; 31 | iofingerprint.width = template.size.x; 32 | iofingerprint.height = template.size.y; 33 | iofingerprint.resolutionX = resolution; 34 | iofingerprint.resolutionY = resolution; 35 | iofingerprint.minutiae = template.minutiae.stream() 36 | .map(m -> encode(m)) 37 | .collect(toList()); 38 | return iofingerprint; 39 | } 40 | private static FeatureTemplate decode(Ansi378v2009Am1Fingerprint iofingerprint) { 41 | TemplateResolution resolution = new TemplateResolution(); 42 | resolution.dpiX = iofingerprint.resolutionX * 2.54; 43 | resolution.dpiY = iofingerprint.resolutionY * 2.54; 44 | return new FeatureTemplate( 45 | resolution.decode(iofingerprint.width, iofingerprint.height), 46 | iofingerprint.minutiae.stream() 47 | .map(m -> decode(m, resolution)) 48 | .collect(toList())); 49 | } 50 | private static Ansi378v2009Am1Minutia encode(FeatureMinutia minutia) { 51 | Ansi378v2009Am1Minutia iominutia = new Ansi378v2009Am1Minutia(); 52 | iominutia.positionX = minutia.position.x; 53 | iominutia.positionY = minutia.position.y; 54 | iominutia.angle = encodeAngle(minutia.direction); 55 | iominutia.type = encode(minutia.type); 56 | return iominutia; 57 | } 58 | private static FeatureMinutia decode(Ansi378v2009Am1Minutia iominutia, TemplateResolution resolution) { 59 | return new FeatureMinutia( 60 | resolution.decode(iominutia.positionX, iominutia.positionY), 61 | decodeAngle(iominutia.angle), 62 | decode(iominutia.type)); 63 | } 64 | private static int encodeAngle(float angle) { 65 | return (int)Math.ceil(DoubleAngle.complementary(angle) * DoubleAngle.INV_PI2 * 360 / 2) % 180; 66 | } 67 | private static float decodeAngle(int ioangle) { 68 | return FloatAngle.complementary(((2 * ioangle - 1 + 360) % 360) / 360.0f * FloatAngle.PI2); 69 | } 70 | private static Ansi378v2009Am1MinutiaType encode(MinutiaType type) { 71 | switch (type) { 72 | case ENDING: 73 | return Ansi378v2009Am1MinutiaType.ENDING; 74 | case BIFURCATION: 75 | return Ansi378v2009Am1MinutiaType.BIFURCATION; 76 | default : 77 | return Ansi378v2009Am1MinutiaType.ENDING; 78 | } 79 | } 80 | private static MinutiaType decode(Ansi378v2009Am1MinutiaType iotype) { 81 | switch (iotype) { 82 | case ENDING: 83 | return MinutiaType.ENDING; 84 | case BIFURCATION: 85 | return MinutiaType.BIFURCATION; 86 | default : 87 | return MinutiaType.ENDING; 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/test/java/com/machinezoo/sourceafis/engine/primitives/BooleanMatrixTest.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.primitives; 3 | 4 | import static org.junit.jupiter.api.Assertions.*; 5 | import org.junit.jupiter.api.*; 6 | 7 | public class BooleanMatrixTest { 8 | private final BooleanMatrix m = new BooleanMatrix(4, 5); 9 | public BooleanMatrixTest() { 10 | for (int x = 0; x < m.width; ++x) 11 | for (int y = 0; y < m.height; ++y) 12 | m.set(x, y, (x + y) % 2 > 0); 13 | } 14 | @Test 15 | public void constructor() { 16 | assertEquals(4, m.width); 17 | assertEquals(5, m.height); 18 | } 19 | @Test 20 | public void constructorFromPoint() { 21 | BooleanMatrix m = new BooleanMatrix(new IntPoint(4, 5)); 22 | assertEquals(4, m.width); 23 | assertEquals(5, m.height); 24 | } 25 | @Test 26 | public void constructorCloning() { 27 | BooleanMatrix m = new BooleanMatrix(this.m); 28 | assertEquals(4, m.width); 29 | assertEquals(5, m.height); 30 | for (int x = 0; x < m.width; ++x) 31 | for (int y = 0; y < m.height; ++y) 32 | assertEquals(this.m.get(x, y), m.get(x, y)); 33 | } 34 | @Test 35 | public void size() { 36 | assertEquals(4, m.size().x); 37 | assertEquals(5, m.size().y); 38 | } 39 | @Test 40 | public void get() { 41 | assertEquals(true, m.get(1, 4)); 42 | assertEquals(false, m.get(3, 1)); 43 | } 44 | @Test 45 | public void getAt() { 46 | assertEquals(true, m.get(new IntPoint(3, 2))); 47 | assertEquals(false, m.get(new IntPoint(2, 4))); 48 | } 49 | @Test 50 | public void getFallback() { 51 | assertEquals(false, m.get(0, 0, true)); 52 | assertEquals(true, m.get(3, 0, false)); 53 | assertEquals(false, m.get(0, 4, true)); 54 | assertEquals(true, m.get(3, 4, false)); 55 | assertEquals(false, m.get(-1, 4, false)); 56 | assertEquals(true, m.get(-1, 4, true)); 57 | assertEquals(false, m.get(2, -1, false)); 58 | assertEquals(true, m.get(4, 2, true)); 59 | assertEquals(false, m.get(2, 5, false)); 60 | } 61 | @Test 62 | public void getAtFallback() { 63 | assertEquals(false, m.get(new IntPoint(0, 0), true)); 64 | assertEquals(true, m.get(new IntPoint(3, 0), false)); 65 | assertEquals(false, m.get(new IntPoint(0, 4), true)); 66 | assertEquals(true, m.get(new IntPoint(3, 4), false)); 67 | assertEquals(false, m.get(new IntPoint(-1, 2), false)); 68 | assertEquals(true, m.get(new IntPoint(-1, 2), true)); 69 | assertEquals(false, m.get(new IntPoint(0, -1), false)); 70 | assertEquals(true, m.get(new IntPoint(4, 0), true)); 71 | assertEquals(false, m.get(new IntPoint(0, 5), false)); 72 | } 73 | @Test 74 | public void set() { 75 | assertEquals(false, m.get(2, 4)); 76 | m.set(2, 4, true); 77 | assertEquals(true, m.get(2, 4)); 78 | } 79 | @Test 80 | public void setAt() { 81 | assertEquals(true, m.get(1, 2)); 82 | m.set(new IntPoint(1, 2), false); 83 | assertEquals(false, m.get(1, 2)); 84 | } 85 | @Test 86 | public void invert() { 87 | m.invert(); 88 | assertEquals(true, m.get(0, 0)); 89 | assertEquals(false, m.get(3, 0)); 90 | assertEquals(true, m.get(0, 4)); 91 | assertEquals(false, m.get(3, 4)); 92 | assertEquals(true, m.get(1, 3)); 93 | assertEquals(false, m.get(2, 1)); 94 | } 95 | @Test 96 | public void merge() { 97 | assertEquals(true, m.get(3, 2)); 98 | BooleanMatrix o = new BooleanMatrix(4, 5); 99 | for (int x = 0; x < m.width; ++x) 100 | for (int y = 0; y < m.height; ++y) 101 | o.set(x, y, x < 2 && y < 3); 102 | m.merge(o); 103 | assertEquals(true, m.get(0, 0)); 104 | assertEquals(true, m.get(1, 2)); 105 | assertEquals(false, m.get(1, 3)); 106 | assertEquals(true, m.get(3, 2)); 107 | for (int x = 0; x < m.width; ++x) 108 | for (int y = 0; y < m.height; ++y) 109 | assertEquals((x + y) % 2 > 0 || x < 2 && y < 3, m.get(x, y)); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/templates/Iso19794p2v2011Codec.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.templates; 3 | 4 | import static java.util.stream.Collectors.*; 5 | import java.util.*; 6 | import java.util.stream.*; 7 | import com.machinezoo.fingerprintio.iso19794p2v2011.*; 8 | import com.machinezoo.noexception.*; 9 | import com.machinezoo.sourceafis.engine.features.*; 10 | import com.machinezoo.sourceafis.engine.primitives.*; 11 | 12 | class Iso19794p2v2011Codec extends TemplateCodec { 13 | @Override 14 | public byte[] encode(List templates) { 15 | Iso19794p2v2011Template iotemplate = new Iso19794p2v2011Template(); 16 | iotemplate.fingerprints = IntStream.range(0, templates.size()) 17 | .mapToObj(n -> encode(n, templates.get(n))) 18 | .collect(toList()); 19 | return iotemplate.toByteArray(); 20 | } 21 | @Override 22 | public List decode(byte[] serialized, ExceptionHandler handler) { 23 | Iso19794p2v2011Template iotemplate = new Iso19794p2v2011Template(serialized, handler); 24 | return iotemplate.fingerprints.stream() 25 | .map(fp -> decode(fp)) 26 | .collect(toList()); 27 | } 28 | private static Iso19794p2v2011Fingerprint encode(int offset, FeatureTemplate template) { 29 | int resolution = (int)Math.round(500 / 2.54); 30 | Iso19794p2v2011Fingerprint iofingerprint = new Iso19794p2v2011Fingerprint(); 31 | iofingerprint.view = offset; 32 | iofingerprint.width = template.size.x; 33 | iofingerprint.height = template.size.y; 34 | iofingerprint.resolutionX = resolution; 35 | iofingerprint.resolutionY = resolution; 36 | iofingerprint.endingType = Iso19794p2v2011EndingType.RIDGE_SKELETON_ENDPOINT; 37 | iofingerprint.minutiae = template.minutiae.stream() 38 | .map(m -> encode(m)) 39 | .collect(toList()); 40 | return iofingerprint; 41 | } 42 | private static FeatureTemplate decode(Iso19794p2v2011Fingerprint iofingerprint) { 43 | TemplateResolution resolution = new TemplateResolution(); 44 | resolution.dpiX = iofingerprint.resolutionX * 2.54; 45 | resolution.dpiY = iofingerprint.resolutionY * 2.54; 46 | return new FeatureTemplate( 47 | resolution.decode(iofingerprint.width, iofingerprint.height), 48 | iofingerprint.minutiae.stream() 49 | .map(m -> decode(m, resolution)) 50 | .collect(toList())); 51 | } 52 | private static Iso19794p2v2011Minutia encode(FeatureMinutia minutia) { 53 | Iso19794p2v2011Minutia iominutia = new Iso19794p2v2011Minutia(); 54 | iominutia.positionX = minutia.position.x; 55 | iominutia.positionY = minutia.position.y; 56 | iominutia.angle = encodeAngle(minutia.direction); 57 | iominutia.type = encode(minutia.type); 58 | return iominutia; 59 | } 60 | private static FeatureMinutia decode(Iso19794p2v2011Minutia iominutia, TemplateResolution resolution) { 61 | return new FeatureMinutia( 62 | resolution.decode(iominutia.positionX, iominutia.positionY), 63 | decodeAngle(iominutia.angle), 64 | decode(iominutia.type)); 65 | } 66 | private static int encodeAngle(float angle) { 67 | return (int)Math.round(DoubleAngle.complementary(angle) * DoubleAngle.INV_PI2 * 256) & 0xff; 68 | } 69 | private static float decodeAngle(int ioangle) { 70 | return FloatAngle.complementary(ioangle / 256.0f * FloatAngle.PI2); 71 | } 72 | private static Iso19794p2v2011MinutiaType encode(MinutiaType type) { 73 | switch (type) { 74 | case ENDING: 75 | return Iso19794p2v2011MinutiaType.ENDING; 76 | case BIFURCATION: 77 | return Iso19794p2v2011MinutiaType.BIFURCATION; 78 | default : 79 | return Iso19794p2v2011MinutiaType.ENDING; 80 | } 81 | } 82 | private static MinutiaType decode(Iso19794p2v2011MinutiaType iotype) { 83 | switch (iotype) { 84 | case ENDING: 85 | return MinutiaType.ENDING; 86 | case BIFURCATION: 87 | return MinutiaType.BIFURCATION; 88 | default : 89 | return MinutiaType.ENDING; 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/templates/Ansi378v2004Codec.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.templates; 3 | 4 | import static java.util.stream.Collectors.*; 5 | import java.util.*; 6 | import java.util.stream.*; 7 | import com.machinezoo.fingerprintio.ansi378v2004.*; 8 | import com.machinezoo.noexception.*; 9 | import com.machinezoo.sourceafis.engine.features.*; 10 | import com.machinezoo.sourceafis.engine.primitives.*; 11 | 12 | class Ansi378v2004Codec extends TemplateCodec { 13 | @Override 14 | public byte[] encode(List templates) { 15 | int resolution = (int)Math.round(500 / 2.54); 16 | Ansi378v2004Template iotemplate = new Ansi378v2004Template(); 17 | iotemplate.width = templates.stream().mapToInt(t -> t.size.x).max().orElse(500); 18 | iotemplate.height = templates.stream().mapToInt(t -> t.size.y).max().orElse(500); 19 | iotemplate.resolutionX = resolution; 20 | iotemplate.resolutionY = resolution; 21 | iotemplate.fingerprints = IntStream.range(0, templates.size()) 22 | .mapToObj(n -> encode(n, templates.get(n))) 23 | .collect(toList()); 24 | return iotemplate.toByteArray(); 25 | } 26 | @Override 27 | public List decode(byte[] serialized, ExceptionHandler handler) { 28 | Ansi378v2004Template iotemplate = new Ansi378v2004Template(serialized, handler); 29 | TemplateResolution resolution = new TemplateResolution(); 30 | resolution.dpiX = iotemplate.resolutionX * 2.54; 31 | resolution.dpiY = iotemplate.resolutionY * 2.54; 32 | return iotemplate.fingerprints.stream() 33 | .map(fp -> decode(fp, iotemplate, resolution)) 34 | .collect(toList()); 35 | } 36 | private static Ansi378v2004Fingerprint encode(int offset, FeatureTemplate template) { 37 | Ansi378v2004Fingerprint iofingerprint = new Ansi378v2004Fingerprint(); 38 | iofingerprint.view = offset; 39 | iofingerprint.minutiae = template.minutiae.stream() 40 | .map(m -> encode(m)) 41 | .collect(toList()); 42 | return iofingerprint; 43 | } 44 | private static FeatureTemplate decode(Ansi378v2004Fingerprint iofingerprint, Ansi378v2004Template iotemplate, TemplateResolution resolution) { 45 | return new FeatureTemplate( 46 | resolution.decode(iotemplate.width, iotemplate.height), 47 | iofingerprint.minutiae.stream() 48 | .map(m -> decode(m, resolution)) 49 | .collect(toList())); 50 | } 51 | private static Ansi378v2004Minutia encode(FeatureMinutia minutia) { 52 | Ansi378v2004Minutia iominutia = new Ansi378v2004Minutia(); 53 | iominutia.positionX = minutia.position.x; 54 | iominutia.positionY = minutia.position.y; 55 | iominutia.angle = encodeAngle(minutia.direction); 56 | iominutia.type = encode(minutia.type); 57 | return iominutia; 58 | } 59 | private static FeatureMinutia decode(Ansi378v2004Minutia iominutia, TemplateResolution resolution) { 60 | return new FeatureMinutia( 61 | resolution.decode(iominutia.positionX, iominutia.positionY), 62 | decodeAngle(iominutia.angle), 63 | decode(iominutia.type)); 64 | } 65 | private static int encodeAngle(float angle) { 66 | return (int)Math.ceil(DoubleAngle.complementary(angle) * DoubleAngle.INV_PI2 * 360 / 2) % 180; 67 | } 68 | private static float decodeAngle(int ioangle) { 69 | return FloatAngle.complementary(((2 * ioangle - 1 + 360) % 360) / 360.0f * FloatAngle.PI2); 70 | } 71 | private static Ansi378v2004MinutiaType encode(MinutiaType type) { 72 | switch (type) { 73 | case ENDING: 74 | return Ansi378v2004MinutiaType.ENDING; 75 | case BIFURCATION: 76 | return Ansi378v2004MinutiaType.BIFURCATION; 77 | default : 78 | return Ansi378v2004MinutiaType.ENDING; 79 | } 80 | } 81 | private static MinutiaType decode(Ansi378v2004MinutiaType iotype) { 82 | switch (iotype) { 83 | case ENDING: 84 | return MinutiaType.ENDING; 85 | case BIFURCATION: 86 | return MinutiaType.BIFURCATION; 87 | default : 88 | return MinutiaType.ENDING; 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/extractor/skeletons/BinaryThinning.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.extractor.skeletons; 3 | 4 | import com.machinezoo.sourceafis.engine.configuration.*; 5 | import com.machinezoo.sourceafis.engine.features.*; 6 | import com.machinezoo.sourceafis.engine.primitives.*; 7 | import com.machinezoo.sourceafis.engine.transparency.*; 8 | 9 | public class BinaryThinning { 10 | private static enum NeighborhoodType { 11 | SKELETON, 12 | ENDING, 13 | REMOVABLE 14 | } 15 | private static NeighborhoodType[] neighborhoodTypes() { 16 | NeighborhoodType[] types = new NeighborhoodType[256]; 17 | for (int mask = 0; mask < 256; ++mask) { 18 | boolean TL = (mask & 1) != 0; 19 | boolean TC = (mask & 2) != 0; 20 | boolean TR = (mask & 4) != 0; 21 | boolean CL = (mask & 8) != 0; 22 | boolean CR = (mask & 16) != 0; 23 | boolean BL = (mask & 32) != 0; 24 | boolean BC = (mask & 64) != 0; 25 | boolean BR = (mask & 128) != 0; 26 | int count = Integer.bitCount(mask); 27 | boolean diagonal = !TC && !CL && TL || !CL && !BC && BL || !BC && !CR && BR || !CR && !TC && TR; 28 | boolean horizontal = !TC && !BC && (TR || CR || BR) && (TL || CL || BL); 29 | boolean vertical = !CL && !CR && (TL || TC || TR) && (BL || BC || BR); 30 | boolean end = (count == 1); 31 | if (end) 32 | types[mask] = NeighborhoodType.ENDING; 33 | else if (!diagonal && !horizontal && !vertical) 34 | types[mask] = NeighborhoodType.REMOVABLE; 35 | else 36 | types[mask] = NeighborhoodType.SKELETON; 37 | } 38 | return types; 39 | } 40 | private static boolean isFalseEnding(BooleanMatrix binary, IntPoint ending) { 41 | for (IntPoint relativeNeighbor : IntPoint.CORNER_NEIGHBORS) { 42 | IntPoint neighbor = ending.plus(relativeNeighbor); 43 | if (binary.get(neighbor)) { 44 | int count = 0; 45 | for (IntPoint relative2 : IntPoint.CORNER_NEIGHBORS) 46 | if (binary.get(neighbor.plus(relative2), false)) 47 | ++count; 48 | return count > 2; 49 | } 50 | } 51 | return false; 52 | } 53 | public static BooleanMatrix thin(BooleanMatrix input, SkeletonType type) { 54 | var neighborhoodTypes = neighborhoodTypes(); 55 | var size = input.size(); 56 | var partial = new BooleanMatrix(size); 57 | for (int y = 1; y < size.y - 1; ++y) 58 | for (int x = 1; x < size.x - 1; ++x) 59 | partial.set(x, y, input.get(x, y)); 60 | var thinned = new BooleanMatrix(size); 61 | boolean removedAnything = true; 62 | for (int i = 0; i < Parameters.THINNING_ITERATIONS && removedAnything; ++i) { 63 | removedAnything = false; 64 | for (int evenY = 0; evenY < 2; ++evenY) 65 | for (int evenX = 0; evenX < 2; ++evenX) 66 | for (int y = 1 + evenY; y < size.y - 1; y += 2) 67 | for (int x = 1 + evenX; x < size.x - 1; x += 2) 68 | if (partial.get(x, y) && !thinned.get(x, y) && !(partial.get(x, y - 1) && partial.get(x, y + 1) && partial.get(x - 1, y) && partial.get(x + 1, y))) { 69 | int neighbors = (partial.get(x + 1, y + 1) ? 128 : 0) 70 | | (partial.get(x, y + 1) ? 64 : 0) 71 | | (partial.get(x - 1, y + 1) ? 32 : 0) 72 | | (partial.get(x + 1, y) ? 16 : 0) 73 | | (partial.get(x - 1, y) ? 8 : 0) 74 | | (partial.get(x + 1, y - 1) ? 4 : 0) 75 | | (partial.get(x, y - 1) ? 2 : 0) 76 | | (partial.get(x - 1, y - 1) ? 1 : 0); 77 | if (neighborhoodTypes[neighbors] == NeighborhoodType.REMOVABLE 78 | || neighborhoodTypes[neighbors] == NeighborhoodType.ENDING 79 | && isFalseEnding(partial, new IntPoint(x, y))) { 80 | removedAnything = true; 81 | partial.set(x, y, false); 82 | } else 83 | thinned.set(x, y, true); 84 | } 85 | } 86 | // https://sourceafis.machinezoo.com/transparency/thinned-skeleton 87 | TransparencySink.current().log(type.prefix + "thinned-skeleton", thinned); 88 | return thinned; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/templates/Iso19794p2v2005Codec.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.templates; 3 | 4 | import static java.util.stream.Collectors.*; 5 | import java.util.*; 6 | import java.util.stream.*; 7 | import com.machinezoo.fingerprintio.iso19794p2v2005.*; 8 | import com.machinezoo.noexception.*; 9 | import com.machinezoo.sourceafis.engine.features.*; 10 | import com.machinezoo.sourceafis.engine.primitives.*; 11 | 12 | class Iso19794p2v2005Codec extends TemplateCodec { 13 | @Override 14 | public byte[] encode(List templates) { 15 | int resolution = (int)Math.round(500 / 2.54); 16 | Iso19794p2v2005Template iotemplate = new Iso19794p2v2005Template(); 17 | iotemplate.width = templates.stream().mapToInt(t -> t.size.x).max().orElse(500); 18 | iotemplate.height = templates.stream().mapToInt(t -> t.size.y).max().orElse(500); 19 | iotemplate.resolutionX = resolution; 20 | iotemplate.resolutionY = resolution; 21 | iotemplate.fingerprints = IntStream.range(0, templates.size()) 22 | .mapToObj(n -> encode(n, templates.get(n))) 23 | .collect(toList()); 24 | return iotemplate.toByteArray(); 25 | } 26 | @Override 27 | public List decode(byte[] serialized, ExceptionHandler handler) { 28 | Iso19794p2v2005Template iotemplate = new Iso19794p2v2005Template(serialized, handler); 29 | TemplateResolution resolution = new TemplateResolution(); 30 | resolution.dpiX = iotemplate.resolutionX * 2.54; 31 | resolution.dpiY = iotemplate.resolutionY * 2.54; 32 | return iotemplate.fingerprints.stream() 33 | .map(fp -> decode(fp, iotemplate, resolution)) 34 | .collect(toList()); 35 | } 36 | private static Iso19794p2v2005Fingerprint encode(int offset, FeatureTemplate template) { 37 | Iso19794p2v2005Fingerprint iofingerprint = new Iso19794p2v2005Fingerprint(); 38 | iofingerprint.view = offset; 39 | iofingerprint.minutiae = template.minutiae.stream() 40 | .map(m -> encode(m)) 41 | .collect(toList()); 42 | return iofingerprint; 43 | } 44 | private static FeatureTemplate decode(Iso19794p2v2005Fingerprint iofingerprint, Iso19794p2v2005Template iotemplate, TemplateResolution resolution) { 45 | return new FeatureTemplate( 46 | resolution.decode(iotemplate.width, iotemplate.height), 47 | iofingerprint.minutiae.stream() 48 | .map(m -> decode(m, resolution)) 49 | .collect(toList())); 50 | } 51 | private static Iso19794p2v2005Minutia encode(FeatureMinutia minutia) { 52 | Iso19794p2v2005Minutia iominutia = new Iso19794p2v2005Minutia(); 53 | iominutia.positionX = minutia.position.x; 54 | iominutia.positionY = minutia.position.y; 55 | iominutia.angle = encodeAngle(minutia.direction); 56 | iominutia.type = encode(minutia.type); 57 | return iominutia; 58 | } 59 | private static FeatureMinutia decode(Iso19794p2v2005Minutia iominutia, TemplateResolution resolution) { 60 | return new FeatureMinutia( 61 | resolution.decode(iominutia.positionX, iominutia.positionY), 62 | decodeAngle(iominutia.angle), 63 | decode(iominutia.type)); 64 | } 65 | private static int encodeAngle(float angle) { 66 | return (int)Math.round(DoubleAngle.complementary(angle) * DoubleAngle.INV_PI2 * 256) & 0xff; 67 | } 68 | private static float decodeAngle(int ioangle) { 69 | return FloatAngle.complementary(ioangle / 256.0f * FloatAngle.PI2); 70 | } 71 | private static Iso19794p2v2005MinutiaType encode(MinutiaType type) { 72 | switch (type) { 73 | case ENDING: 74 | return Iso19794p2v2005MinutiaType.ENDING; 75 | case BIFURCATION: 76 | return Iso19794p2v2005MinutiaType.BIFURCATION; 77 | default : 78 | return Iso19794p2v2005MinutiaType.ENDING; 79 | } 80 | } 81 | private static MinutiaType decode(Iso19794p2v2005MinutiaType iotype) { 82 | switch (iotype) { 83 | case ENDING: 84 | return MinutiaType.ENDING; 85 | case BIFURCATION: 86 | return MinutiaType.BIFURCATION; 87 | default : 88 | return MinutiaType.ENDING; 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/matcher/EdgeHashes.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.matcher; 3 | 4 | import java.util.*; 5 | import com.machinezoo.sourceafis.engine.configuration.*; 6 | import com.machinezoo.sourceafis.engine.features.*; 7 | import com.machinezoo.sourceafis.engine.primitives.*; 8 | import com.machinezoo.sourceafis.engine.templates.*; 9 | import com.machinezoo.sourceafis.engine.transparency.*; 10 | import it.unimi.dsi.fastutil.ints.*; 11 | 12 | public class EdgeHashes { 13 | private static final float COMPLEMENTARY_MAX_ANGLE_ERROR = FloatAngle.complementary(Parameters.MAX_ANGLE_ERROR); 14 | public static int hash(EdgeShape edge) { 15 | int lengthBin = edge.length / Parameters.MAX_DISTANCE_ERROR; 16 | int referenceAngleBin = (int)(edge.referenceAngle / Parameters.MAX_ANGLE_ERROR); 17 | int neighborAngleBin = (int)(edge.neighborAngle / Parameters.MAX_ANGLE_ERROR); 18 | return (referenceAngleBin << 24) + (neighborAngleBin << 16) + lengthBin; 19 | } 20 | public static boolean matching(EdgeShape probe, EdgeShape candidate) { 21 | int lengthDelta = probe.length - candidate.length; 22 | if (lengthDelta >= -Parameters.MAX_DISTANCE_ERROR && lengthDelta <= Parameters.MAX_DISTANCE_ERROR) { 23 | float referenceDelta = FloatAngle.difference(probe.referenceAngle, candidate.referenceAngle); 24 | if (referenceDelta <= Parameters.MAX_ANGLE_ERROR || referenceDelta >= COMPLEMENTARY_MAX_ANGLE_ERROR) { 25 | float neighborDelta = FloatAngle.difference(probe.neighborAngle, candidate.neighborAngle); 26 | if (neighborDelta <= Parameters.MAX_ANGLE_ERROR || neighborDelta >= COMPLEMENTARY_MAX_ANGLE_ERROR) 27 | return true; 28 | } 29 | } 30 | return false; 31 | } 32 | private static List coverage(EdgeShape edge) { 33 | int minLengthBin = (edge.length - Parameters.MAX_DISTANCE_ERROR) / Parameters.MAX_DISTANCE_ERROR; 34 | int maxLengthBin = (edge.length + Parameters.MAX_DISTANCE_ERROR) / Parameters.MAX_DISTANCE_ERROR; 35 | int angleBins = (int)Math.ceil(2 * Math.PI / Parameters.MAX_ANGLE_ERROR); 36 | int minReferenceBin = (int)(FloatAngle.difference(edge.referenceAngle, Parameters.MAX_ANGLE_ERROR) / Parameters.MAX_ANGLE_ERROR); 37 | int maxReferenceBin = (int)(FloatAngle.add(edge.referenceAngle, Parameters.MAX_ANGLE_ERROR) / Parameters.MAX_ANGLE_ERROR); 38 | int endReferenceBin = (maxReferenceBin + 1) % angleBins; 39 | int minNeighborBin = (int)(FloatAngle.difference(edge.neighborAngle, Parameters.MAX_ANGLE_ERROR) / Parameters.MAX_ANGLE_ERROR); 40 | int maxNeighborBin = (int)(FloatAngle.add(edge.neighborAngle, Parameters.MAX_ANGLE_ERROR) / Parameters.MAX_ANGLE_ERROR); 41 | int endNeighborBin = (maxNeighborBin + 1) % angleBins; 42 | List coverage = new ArrayList<>(); 43 | for (int lengthBin = minLengthBin; lengthBin <= maxLengthBin; ++lengthBin) 44 | for (int referenceBin = minReferenceBin; referenceBin != endReferenceBin; referenceBin = (referenceBin + 1) % angleBins) 45 | for (int neighborBin = minNeighborBin; neighborBin != endNeighborBin; neighborBin = (neighborBin + 1) % angleBins) 46 | coverage.add((referenceBin << 24) + (neighborBin << 16) + lengthBin); 47 | return coverage; 48 | } 49 | public static Int2ObjectMap> build(SearchTemplate template) { 50 | Int2ObjectMap> map = new Int2ObjectOpenHashMap<>(); 51 | for (int reference = 0; reference < template.minutiae.length; ++reference) 52 | for (int neighbor = 0; neighbor < template.minutiae.length; ++neighbor) 53 | if (reference != neighbor) { 54 | IndexedEdge edge = new IndexedEdge(template.minutiae, reference, neighbor); 55 | for (int hash : coverage(edge)) { 56 | List list = map.get(hash); 57 | if (list == null) 58 | map.put(hash, list = new ArrayList<>()); 59 | list.add(edge); 60 | } 61 | } 62 | // https://sourceafis.machinezoo.com/transparency/edge-hash 63 | TransparencySink.current().logEdgeHash(map); 64 | return map; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/primitives/MemoryEstimates.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.primitives; 3 | 4 | /* 5 | * JOL library can measure memory footprint of objects, 6 | * but its recent versions require special permissions (granted on command line or otherwise) 7 | * and it is not a Java module. It now prints ugly warnings to the console. 8 | * JOL development is essentially halted. 9 | * So we use our own somewhat crude estimate instead. 10 | */ 11 | public class MemoryEstimates { 12 | private static int detectBitness() { 13 | /* 14 | * Source: https://www.baeldung.com/java-detect-jvm-64-or-32-bit 15 | * This will work on Oracle Java only. 16 | */ 17 | var model = System.getProperty("sun.arch.data.model", "unknown"); 18 | if (model.equals("32")) 19 | return 32; 20 | if (model.equals("64")) 21 | return 64; 22 | var arch = System.getProperty("os.arch", "unknown"); 23 | switch (arch) { 24 | /* 25 | * Source of constants: https://stackoverflow.com/a/2062045 26 | */ 27 | case "x86": 28 | return 32; 29 | case "amd64": 30 | return 64; 31 | /* 32 | * Source of constants: https://github.com/tnakamot/java-os-detector/blob/master/src/main/java/com/github/tnakamot/os/Detector.java 33 | * Patterns that match '32' or '64' are handled in general way below. 34 | */ 35 | case "ia32e": 36 | return 64; 37 | case "i386": 38 | case "i486": 39 | case "i586": 40 | case "i686": 41 | case "ia64n": 42 | return 32; 43 | case "sparc": 44 | case "arm": 45 | case "mips": 46 | case "mipsel": 47 | case "ppc": 48 | case "ppcle": 49 | case "s390": 50 | return 32; 51 | default: 52 | if (arch.contains("64")) 53 | return 64; 54 | if (arch.contains("32")) 55 | return 32; 56 | /* 57 | * Assume 64-bit JVM unless we have proof to the contrary. 58 | */ 59 | return 64; 60 | } 61 | } 62 | private static final int BITNESS = detectBitness(); 63 | /* 64 | * Assume compressed 32-bit references even on 64-bit platforms. 65 | * This will be wrong on 32GB+ heaps or when compressed references are disabled (-XX:-UseCompressedOops). 66 | */ 67 | public static final int REFERENCE = 4; 68 | /* 69 | * Mark word in standard object layout matches platform bitness. 70 | */ 71 | private static final int MARK = BITNESS / 8; 72 | /* 73 | * Assume standard object layout: mark word + class pointer. 74 | */ 75 | private static final int OBJECT_HEADER = MARK + REFERENCE; 76 | /* 77 | * Assume that padding ensures alignment of longs and doubles even on 32-bit platforms. 78 | */ 79 | private static final int PADDING = 8; 80 | private static int pad(int padding, int size) { return (size + padding - 1) / padding * padding; } 81 | private static int pad(int size) { return pad(PADDING, size); } 82 | /* 83 | * Assume optimal field layout in memory. 84 | * Alignment refers to minimum alignment of the field block, which is usually equal to the largest field. 85 | */ 86 | public static int object(int fields, int alignment) { return pad(pad(alignment, OBJECT_HEADER) + fields); } 87 | /* 88 | * Assume standard array layout: object header + 32-bit length. 89 | */ 90 | private static final int ARRAY_HEADER = OBJECT_HEADER + 4; 91 | public static int array(int component, int count) { 92 | /* 93 | * Pad array header to ensure alignment of all array items. 94 | */ 95 | return pad(pad(component, ARRAY_HEADER) + component * count); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/primitives/IntPoint.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.primitives; 3 | 4 | import java.util.*; 5 | 6 | public class IntPoint implements Iterable, Comparable { 7 | public static final IntPoint ZERO = new IntPoint(0, 0); 8 | public static final IntPoint[] EDGE_NEIGHBORS = new IntPoint[] { 9 | new IntPoint(0, -1), 10 | new IntPoint(-1, 0), 11 | new IntPoint(1, 0), 12 | new IntPoint(0, 1) 13 | }; 14 | public static final IntPoint[] CORNER_NEIGHBORS = new IntPoint[] { 15 | new IntPoint(-1, -1), 16 | new IntPoint(0, -1), 17 | new IntPoint(1, -1), 18 | new IntPoint(-1, 0), 19 | new IntPoint(1, 0), 20 | new IntPoint(-1, 1), 21 | new IntPoint(0, 1), 22 | new IntPoint(1, 1) 23 | }; 24 | public final int x; 25 | public final int y; 26 | public IntPoint(int x, int y) { 27 | this.x = x; 28 | this.y = y; 29 | } 30 | public int area() { 31 | return x * y; 32 | } 33 | public int lengthSq() { 34 | return Integers.sq(x) + Integers.sq(y); 35 | } 36 | public boolean contains(IntPoint other) { 37 | return other.x >= 0 && other.y >= 0 && other.x < x && other.y < y; 38 | } 39 | public IntPoint plus(IntPoint other) { 40 | return new IntPoint(x + other.x, y + other.y); 41 | } 42 | public IntPoint minus(IntPoint other) { 43 | return new IntPoint(x - other.x, y - other.y); 44 | } 45 | public IntPoint negate() { 46 | return new IntPoint(-x, -y); 47 | } 48 | public DoublePoint toDouble() { 49 | return new DoublePoint(x, y); 50 | } 51 | public IntPoint[] lineTo(IntPoint to) { 52 | IntPoint[] result; 53 | IntPoint relative = to.minus(this); 54 | if (Math.abs(relative.x) >= Math.abs(relative.y)) { 55 | result = new IntPoint[Math.abs(relative.x) + 1]; 56 | if (relative.x > 0) { 57 | for (int i = 0; i <= relative.x; ++i) 58 | result[i] = new IntPoint(x + i, y + (int)Math.round(i * (relative.y / (double)relative.x))); 59 | } else if (relative.x < 0) { 60 | for (int i = 0; i <= -relative.x; ++i) 61 | result[i] = new IntPoint(x - i, y - (int)Math.round(i * (relative.y / (double)relative.x))); 62 | } else 63 | result[0] = this; 64 | } else { 65 | result = new IntPoint[Math.abs(relative.y) + 1]; 66 | if (relative.y > 0) { 67 | for (int i = 0; i <= relative.y; ++i) 68 | result[i] = new IntPoint(x + (int)Math.round(i * (relative.x / (double)relative.y)), y + i); 69 | } else if (relative.y < 0) { 70 | for (int i = 0; i <= -relative.y; ++i) 71 | result[i] = new IntPoint(x - (int)Math.round(i * (relative.x / (double)relative.y)), y - i); 72 | } else 73 | result[0] = this; 74 | } 75 | return result; 76 | } 77 | private List fields() { 78 | return Arrays.asList(x, y); 79 | } 80 | @Override 81 | public boolean equals(Object obj) { 82 | return obj instanceof IntPoint && fields().equals(((IntPoint)obj).fields()); 83 | } 84 | @Override 85 | public int hashCode() { 86 | return Objects.hash(x, y); 87 | } 88 | @Override 89 | public int compareTo(IntPoint other) { 90 | int resultY = Integer.compare(y, other.y); 91 | if (resultY != 0) 92 | return resultY; 93 | return Integer.compare(x, other.x); 94 | } 95 | @Override 96 | public String toString() { 97 | return String.format("[%d,%d]", x, y); 98 | } 99 | @Override 100 | public Iterator iterator() { 101 | return new IntPointIterator(); 102 | } 103 | private class IntPointIterator implements Iterator { 104 | int atX; 105 | int atY; 106 | @Override 107 | public boolean hasNext() { 108 | return atY < y && atX < x; 109 | } 110 | @Override 111 | public IntPoint next() { 112 | if (!hasNext()) 113 | throw new NoSuchElementException(); 114 | IntPoint result = new IntPoint(atX, atY); 115 | ++atX; 116 | if (atX >= x) { 117 | atX = 0; 118 | ++atY; 119 | } 120 | return result; 121 | } 122 | @Override 123 | public void remove() { 124 | throw new UnsupportedOperationException(); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/extractor/PixelwiseOrientations.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.extractor; 3 | 4 | import java.util.*; 5 | import com.machinezoo.sourceafis.engine.configuration.*; 6 | import com.machinezoo.sourceafis.engine.primitives.*; 7 | import com.machinezoo.sourceafis.engine.transparency.*; 8 | 9 | public class PixelwiseOrientations { 10 | private static class ConsideredOrientation { 11 | IntPoint offset; 12 | DoublePoint orientation; 13 | } 14 | private static class OrientationRandom { 15 | static final int PRIME = 1610612741; 16 | static final int BITS = 30; 17 | static final int MASK = (1 << BITS) - 1; 18 | static final double SCALING = 1.0 / (1 << BITS); 19 | long state = PRIME * PRIME * PRIME; 20 | double next() { 21 | state *= PRIME; 22 | return ((state & MASK) + 0.5) * SCALING; 23 | } 24 | } 25 | private static ConsideredOrientation[][] plan() { 26 | OrientationRandom random = new OrientationRandom(); 27 | ConsideredOrientation[][] splits = new ConsideredOrientation[Parameters.ORIENTATION_SPLIT][]; 28 | for (int i = 0; i < Parameters.ORIENTATION_SPLIT; ++i) { 29 | ConsideredOrientation[] orientations = splits[i] = new ConsideredOrientation[Parameters.ORIENTATIONS_CHECKED]; 30 | for (int j = 0; j < Parameters.ORIENTATIONS_CHECKED; ++j) { 31 | ConsideredOrientation sample = orientations[j] = new ConsideredOrientation(); 32 | do { 33 | double angle = random.next() * Math.PI; 34 | double distance = Doubles.interpolateExponential(Parameters.MIN_ORIENTATION_RADIUS, Parameters.MAX_ORIENTATION_RADIUS, random.next()); 35 | sample.offset = DoubleAngle.toVector(angle).multiply(distance).round(); 36 | } while (sample.offset.equals(IntPoint.ZERO) || sample.offset.y < 0 || Arrays.stream(orientations).limit(j).anyMatch(o -> o.offset.equals(sample.offset))); 37 | sample.orientation = DoubleAngle.toVector(DoubleAngle.add(DoubleAngle.toOrientation(DoubleAngle.atan(sample.offset.toDouble())), Math.PI)); 38 | } 39 | } 40 | return splits; 41 | } 42 | private static IntRange maskRange(BooleanMatrix mask, int y) { 43 | int first = -1; 44 | int last = -1; 45 | for (int x = 0; x < mask.width; ++x) 46 | if (mask.get(x, y)) { 47 | last = x; 48 | if (first < 0) 49 | first = x; 50 | } 51 | if (first >= 0) 52 | return new IntRange(first, last + 1); 53 | else 54 | return IntRange.ZERO; 55 | } 56 | public static DoublePointMatrix compute(DoubleMatrix input, BooleanMatrix mask, BlockMap blocks) { 57 | ConsideredOrientation[][] neighbors = plan(); 58 | DoublePointMatrix orientation = new DoublePointMatrix(input.size()); 59 | for (int blockY = 0; blockY < blocks.primary.blocks.y; ++blockY) { 60 | IntRange maskRange = maskRange(mask, blockY); 61 | if (maskRange.length() > 0) { 62 | IntRange validXRange = new IntRange( 63 | blocks.primary.block(maskRange.start, blockY).left(), 64 | blocks.primary.block(maskRange.end - 1, blockY).right()); 65 | for (int y = blocks.primary.block(0, blockY).top(); y < blocks.primary.block(0, blockY).bottom(); ++y) { 66 | for (ConsideredOrientation neighbor : neighbors[y % neighbors.length]) { 67 | int radius = Math.max(Math.abs(neighbor.offset.x), Math.abs(neighbor.offset.y)); 68 | if (y - radius >= 0 && y + radius < input.height) { 69 | IntRange xRange = new IntRange(Math.max(radius, validXRange.start), Math.min(input.width - radius, validXRange.end)); 70 | for (int x = xRange.start; x < xRange.end; ++x) { 71 | double before = input.get(x - neighbor.offset.x, y - neighbor.offset.y); 72 | double at = input.get(x, y); 73 | double after = input.get(x + neighbor.offset.x, y + neighbor.offset.y); 74 | double strength = at - Math.max(before, after); 75 | if (strength > 0) 76 | orientation.add(x, y, neighbor.orientation.multiply(strength)); 77 | } 78 | } 79 | } 80 | } 81 | } 82 | } 83 | // https://sourceafis.machinezoo.com/transparency/pixelwise-orientation 84 | TransparencySink.current().log("pixelwise-orientation", orientation); 85 | return orientation; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/com/machinezoo/sourceafis/engine/configuration/Parameters.java: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for Java: https://sourceafis.machinezoo.com/java 2 | package com.machinezoo.sourceafis.engine.configuration; 3 | 4 | import com.machinezoo.sourceafis.engine.primitives.*; 5 | 6 | public class Parameters { 7 | public static final int BLOCK_SIZE = 15; 8 | public static final int HISTOGRAM_DEPTH = 256; 9 | public static final double CLIPPED_CONTRAST = 0.08; 10 | public static final double MIN_ABSOLUTE_CONTRAST = 17 / 255.0; 11 | public static final double MIN_RELATIVE_CONTRAST = 0.34; 12 | public static final int RELATIVE_CONTRAST_SAMPLE = 168568; 13 | public static final double RELATIVE_CONTRAST_PERCENTILE = 0.49; 14 | public static final int MASK_VOTE_RADIUS = 7; 15 | public static final double MASK_VOTE_MAJORITY = 0.51; 16 | public static final int MASK_VOTE_BORDER_DISTANCE = 4; 17 | public static final int BLOCK_ERRORS_VOTE_RADIUS = 1; 18 | public static final double BLOCK_ERRORS_VOTE_MAJORITY = 0.7; 19 | public static final int BLOCK_ERRORS_VOTE_BORDER_DISTANCE = 4; 20 | public static final double MAX_EQUALIZATION_SCALING = 3.99; 21 | public static final double MIN_EQUALIZATION_SCALING = 0.25; 22 | public static final double MIN_ORIENTATION_RADIUS = 2; 23 | public static final double MAX_ORIENTATION_RADIUS = 6; 24 | public static final int ORIENTATION_SPLIT = 50; 25 | public static final int ORIENTATIONS_CHECKED = 20; 26 | public static final int ORIENTATION_SMOOTHING_RADIUS = 1; 27 | public static final int PARALLEL_SMOOTHING_RESOLUTION = 32; 28 | public static final int PARALLEL_SMOOTHING_RADIUS = 7; 29 | public static final double PARALLEL_SMOOTHING_STEP = 1.59; 30 | public static final int ORTHOGONAL_SMOOTHING_RESOLUTION = 11; 31 | public static final int ORTHOGONAL_SMOOTHING_RADIUS = 4; 32 | public static final double ORTHOGONAL_SMOOTHING_STEP = 1.11; 33 | public static final int BINARIZED_VOTE_RADIUS = 2; 34 | public static final double BINARIZED_VOTE_MAJORITY = 0.61; 35 | public static final int BINARIZED_VOTE_BORDER_DISTANCE = 17; 36 | public static final int INNER_MASK_BORDER_DISTANCE = 14; 37 | public static final double MASK_DISPLACEMENT = 10.06; 38 | public static final int MINUTIA_CLOUD_RADIUS = 20; 39 | public static final int MAX_CLOUD_SIZE = 4; 40 | public static final int MAX_MINUTIAE = 100; 41 | public static final int SORT_BY_NEIGHBOR = 5; 42 | public static final int EDGE_TABLE_NEIGHBORS = 9; 43 | public static final int THINNING_ITERATIONS = 26; 44 | public static final int MAX_PORE_ARM = 41; 45 | public static final int SHORTEST_JOINED_ENDING = 7; 46 | public static final int MAX_RUPTURE_SIZE = 5; 47 | public static final int MAX_GAP_SIZE = 20; 48 | public static final int GAP_ANGLE_OFFSET = 22; 49 | public static final int TOLERATED_GAP_OVERLAP = 2; 50 | public static final int MIN_TAIL_LENGTH = 21; 51 | public static final int MIN_FRAGMENT_LENGTH = 22; 52 | public static final int MAX_DISTANCE_ERROR = 13; 53 | public static final float MAX_ANGLE_ERROR = FloatAngle.PI / 180 * 10; 54 | public static final double MAX_GAP_ANGLE = Math.toRadians(45); 55 | public static final int RIDGE_DIRECTION_SAMPLE = 21; 56 | public static final int RIDGE_DIRECTION_SKIP = 1; 57 | public static final int MAX_TRIED_ROOTS = 70; 58 | public static final int MIN_ROOT_EDGE_LENGTH = 58; 59 | public static final int MAX_ROOT_EDGE_LOOKUPS = 1633; 60 | public static final int MIN_SUPPORTING_EDGES = 1; 61 | public static final double DISTANCE_ERROR_FLATNESS = 0.69; 62 | public static final double ANGLE_ERROR_FLATNESS = 0.27; 63 | public static final double MINUTIA_SCORE = 0.032; 64 | public static final double MINUTIA_FRACTION_SCORE = 8.98; 65 | public static final double MINUTIA_TYPE_SCORE = 0.629; 66 | public static final double SUPPORTED_MINUTIA_SCORE = 0.193; 67 | public static final double EDGE_SCORE = 0.265; 68 | public static final double DISTANCE_ACCURACY_SCORE = 9.9; 69 | public static final double ANGLE_ACCURACY_SCORE = 2.79; 70 | public static final double THRESHOLD_FMR_MAX = 8.48; 71 | public static final double THRESHOLD_FMR_2 = 11.12; 72 | public static final double THRESHOLD_FMR_10 = 14.15; 73 | public static final double THRESHOLD_FMR_100 = 18.22; 74 | public static final double THRESHOLD_FMR_1000 = 22.39; 75 | public static final double THRESHOLD_FMR_10_000 = 27.24; 76 | public static final double THRESHOLD_FMR_100_000 = 32.01; 77 | } 78 | --------------------------------------------------------------------------------