├── .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 | [](https://github.com/vshymanskyy/StandWithUkraine/blob/main/docs/README.md)
2 |
3 | # SourceAFIS for Java
4 |
5 | [](https://central.sonatype.com/artifact/com.machinezoo.sourceafis/sourceafis)
6 | [](https://github.com/robertvazan/sourceafis-java/actions/workflows/build.yml)
7 | [](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