├── .gitattributes ├── SourceAFIS ├── icon.png ├── Engine │ ├── Features │ │ ├── SkeletonType.cs │ │ ├── MinutiaType.cs │ │ ├── SkeletonTypes.cs │ │ ├── IndexedEdge.cs │ │ ├── SkeletonMinutia.cs │ │ ├── Minutia.cs │ │ ├── Skeleton.cs │ │ ├── SkeletonRidge.cs │ │ ├── NeighborEdge.cs │ │ └── EdgeShape.cs │ ├── Transparency │ │ ├── ConsistentMinutiaPair.cs │ │ ├── ConsistentHashEntry.cs │ │ ├── ConsistentSkeletonRidge.cs │ │ ├── ConsistentEdgePair.cs │ │ ├── NoTransparency.cs │ │ ├── ConsistentPairingGraph.cs │ │ └── ConsistentSkeleton.cs │ ├── Templates │ │ ├── FeatureTemplate.cs │ │ └── PersistentTemplate.cs │ ├── Matcher │ │ ├── MinutiaPair.cs │ │ ├── RootList.cs │ │ ├── MinutiaPairPool.cs │ │ ├── ScoringData.cs │ │ ├── MatcherThread.cs │ │ ├── PairingGraph.cs │ │ ├── RootEnumerator.cs │ │ ├── MatcherEngine.cs │ │ ├── EdgeSpider.cs │ │ └── EdgeHashes.cs │ ├── Primitives │ │ ├── IntRange.cs │ │ ├── Doubles.cs │ │ ├── IntMatrix.cs │ │ ├── BlockGrid.cs │ │ ├── DoublePoint.cs │ │ ├── FloatAngle.cs │ │ ├── DoubleMatrix.cs │ │ ├── BlockMap.cs │ │ ├── Integers.cs │ │ ├── HistogramCube.cs │ │ ├── DoublePointMatrix.cs │ │ ├── ReversedList.cs │ │ ├── BooleanMatrix.cs │ │ ├── ShortPoint.cs │ │ ├── CircularList.cs │ │ ├── DoubleAngle.cs │ │ ├── IntRect.cs │ │ ├── PriorityQueue.cs │ │ ├── SerializationUtils.cs │ │ ├── CircularArray.cs │ │ └── IntPoint.cs │ ├── Extractor │ │ ├── Skeletons │ │ │ ├── SkeletonDotFilter.cs │ │ │ ├── SkeletonFilters.cs │ │ │ ├── SkeletonGraphs.cs │ │ │ ├── SkeletonGap.cs │ │ │ ├── SkeletonFragmentFilter.cs │ │ │ ├── SkeletonTailFilter.cs │ │ │ ├── SkeletonKnotFilter.cs │ │ │ ├── SkeletonPoreFilter.cs │ │ │ ├── SkeletonGapFilter.cs │ │ │ └── BinaryThinning.cs │ │ ├── Minutiae │ │ │ ├── InnerMinutiaeFilter.cs │ │ │ ├── MinutiaCloudFilter.cs │ │ │ ├── MinutiaCollector.cs │ │ │ └── TopMinutiaeFilter.cs │ │ ├── AbsoluteContrastMask.cs │ │ ├── RelativeContrastMask.cs │ │ ├── ClippedContrast.cs │ │ ├── ImageResizer.cs │ │ ├── LocalHistograms.cs │ │ ├── BlockOrientations.cs │ │ ├── VoteFilter.cs │ │ ├── FeatureExtractor.cs │ │ ├── SegmentationMask.cs │ │ ├── BinarizedImage.cs │ │ ├── OrientedSmoothing.cs │ │ └── ImageEqualization.cs │ └── Configuration │ │ └── Parameters.cs ├── FingerprintCompatibility.cs ├── SourceAFIS.csproj ├── FingerprintImageOptions.cs └── FingerprintMatcher.cs ├── SourceAFIS.Tests ├── Resources │ ├── probe.bmp │ ├── probe.jpeg │ ├── probe.png │ ├── matching.png │ ├── gray-probe.dat │ ├── nonmatching.png │ ├── gray-matching.dat │ ├── gray-nonmatching.dat │ └── TestResources.cs ├── FingerprintCompatibilityTest.cs ├── Engine │ └── Primitives │ │ ├── IntRangeTest.cs │ │ ├── DoublePointTest.cs │ │ ├── BlockGridTest.cs │ │ ├── BlockMapTest.cs │ │ ├── IntegersTest.cs │ │ ├── SerializationUtilsTest.cs │ │ ├── DoubleMatrixTest.cs │ │ ├── DoublePointMatrixTest.cs │ │ ├── HistogramCubeTest.cs │ │ ├── DoublesTest.cs │ │ ├── CircularListTest.cs │ │ ├── ReversedListTest.cs │ │ ├── IntRectTest.cs │ │ ├── BooleanMatrixTest.cs │ │ └── IntPointTest.cs ├── SourceAFIS.Tests.csproj ├── FingerprintMatcherTest.cs ├── FingerprintTemplateTest.cs ├── FingerprintImageTest.cs └── FingerprintTransparencyTest.cs ├── COPYRIGHT ├── .gitignore ├── .github └── workflows │ ├── build.yml │ └── release.yml ├── CONTRIBUTING.md ├── SourceAFIS.sln └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | *.dat binary 2 | 3 | -------------------------------------------------------------------------------- /SourceAFIS/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertvazan/sourceafis-net/HEAD/SourceAFIS/icon.png -------------------------------------------------------------------------------- /SourceAFIS.Tests/Resources/probe.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertvazan/sourceafis-net/HEAD/SourceAFIS.Tests/Resources/probe.bmp -------------------------------------------------------------------------------- /SourceAFIS.Tests/Resources/probe.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertvazan/sourceafis-net/HEAD/SourceAFIS.Tests/Resources/probe.jpeg -------------------------------------------------------------------------------- /SourceAFIS.Tests/Resources/probe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertvazan/sourceafis-net/HEAD/SourceAFIS.Tests/Resources/probe.png -------------------------------------------------------------------------------- /SourceAFIS.Tests/Resources/matching.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertvazan/sourceafis-net/HEAD/SourceAFIS.Tests/Resources/matching.png -------------------------------------------------------------------------------- /SourceAFIS.Tests/Resources/gray-probe.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertvazan/sourceafis-net/HEAD/SourceAFIS.Tests/Resources/gray-probe.dat -------------------------------------------------------------------------------- /SourceAFIS.Tests/Resources/nonmatching.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertvazan/sourceafis-net/HEAD/SourceAFIS.Tests/Resources/nonmatching.png -------------------------------------------------------------------------------- /SourceAFIS.Tests/Resources/gray-matching.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertvazan/sourceafis-net/HEAD/SourceAFIS.Tests/Resources/gray-matching.dat -------------------------------------------------------------------------------- /SourceAFIS.Tests/Resources/gray-nonmatching.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertvazan/sourceafis-net/HEAD/SourceAFIS.Tests/Resources/gray-nonmatching.dat -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | Robert Važan's SourceAFIS for .NET 2 | https://sourceafis.machinezoo.com/net 3 | 4 | Copyright 2009-2025 Robert Važan and contributors 5 | Distributed under Apache License 2.0. 6 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Features/SkeletonType.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | 3 | namespace SourceAFIS.Engine.Features 4 | { 5 | enum SkeletonType 6 | { 7 | Ridges, 8 | Valleys 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Transparency/ConsistentMinutiaPair.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | 3 | namespace SourceAFIS.Engine.Transparency 4 | { 5 | record ConsistentMinutiaPair(int Probe, int Candidate) 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Features/MinutiaType.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | 3 | namespace SourceAFIS.Engine.Features 4 | { 5 | enum MinutiaType : byte 6 | { 7 | Ending = 0, 8 | Bifurcation = 1 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Transparency/ConsistentHashEntry.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System.Collections.Generic; 3 | using SourceAFIS.Engine.Features; 4 | 5 | namespace SourceAFIS.Engine.Transparency 6 | { 7 | record ConsistentHashEntry(int Key, List Edges) 8 | { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Transparency/ConsistentSkeletonRidge.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System.Collections.Generic; 3 | using SourceAFIS.Engine.Primitives; 4 | 5 | namespace SourceAFIS.Engine.Transparency 6 | { 7 | record ConsistentSkeletonRidge(int Start, int End, IList Points) 8 | { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Templates/FeatureTemplate.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System.Collections.Generic; 3 | using SourceAFIS.Engine.Features; 4 | using SourceAFIS.Engine.Primitives; 5 | 6 | namespace SourceAFIS.Engine.Templates 7 | { 8 | record FeatureTemplate(ShortPoint Size, List Minutiae) 9 | { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /SourceAFIS.Tests/FingerprintCompatibilityTest.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using NUnit.Framework; 3 | 4 | namespace SourceAFIS 5 | { 6 | public class FingerprintCompatibilityTest 7 | { 8 | [Test] 9 | public void Version() => Assert.That(FingerprintCompatibility.Version, Does.Match("^\\d+\\.\\d+\\.\\d+$")); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Transparency/ConsistentEdgePair.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using SourceAFIS.Engine.Matcher; 3 | 4 | namespace SourceAFIS.Engine.Transparency 5 | { 6 | record ConsistentEdgePair(int ProbeFrom, int ProbeTo, int CandidateFrom, int CandidateTo) 7 | { 8 | public ConsistentEdgePair(MinutiaPair pair) : this(pair.ProbeRef, pair.Probe, pair.CandidateRef, pair.Candidate) { } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.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 | # Build results 14 | [Bb]in/ 15 | [Oo]bj/ 16 | [Dd]ebug/ 17 | [Rr]eleases/ 18 | [Rr]elease/ 19 | 20 | # Visual Studio 21 | .vs/ 22 | *.suo 23 | *.user 24 | *.userosscache 25 | *.sln.docstates 26 | 27 | # Rider 28 | *.sln.iml 29 | 30 | # NuGet packages 31 | *.nupkg 32 | *.snupkg 33 | **/[Pp]ackages/ 34 | -------------------------------------------------------------------------------- /.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-dotnet@v4 14 | with: 15 | dotnet-version: '8.0.x' 16 | - name: Build 17 | run: dotnet build 18 | - name: Test 19 | run: dotnet test 20 | - name: Package 21 | run: dotnet pack 22 | -------------------------------------------------------------------------------- /.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-dotnet@v4 10 | with: 11 | dotnet-version: '8.0.x' 12 | - name: Create release package 13 | run: dotnet pack --configuration Release 14 | - name: Publish to NuGet 15 | run: dotnet nuget push */bin/Release/*.nupkg -k ${{ secrets.NUGET_TOKEN }} -s https://api.nuget.org/v3/index.json 16 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Transparency/NoTransparency.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | 3 | namespace SourceAFIS.Engine.Transparency 4 | { 5 | class NoTransparency : FingerprintTransparency 6 | { 7 | public static readonly NoTransparency Instance = new NoTransparency(); 8 | // Dispose it immediately, so that it does not hang around in thread-local variable. 9 | NoTransparency() => Dispose(); 10 | public override bool Accepts(string key) => false; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Matcher/MinutiaPair.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | 3 | namespace SourceAFIS.Engine.Matcher 4 | { 5 | class MinutiaPair 6 | { 7 | public int Probe; 8 | public int Candidate; 9 | public int ProbeRef; 10 | public int CandidateRef; 11 | public int Distance; 12 | public int SupportingEdges; 13 | 14 | public override string ToString() => string.Format("{0}<->{1} @ {2}<->{3} #{4}", Probe, Candidate, ProbeRef, CandidateRef, SupportingEdges); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /SourceAFIS.Tests/Engine/Primitives/IntRangeTest.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using NUnit.Framework; 3 | 4 | namespace SourceAFIS.Engine.Primitives 5 | { 6 | public class IntRangeTest 7 | { 8 | [Test] 9 | public void Constructor() 10 | { 11 | var r = new IntRange(3, 10); 12 | Assert.AreEqual(3, r.Start); 13 | Assert.AreEqual(10, r.End); 14 | } 15 | [Test] 16 | public void Length() => Assert.AreEqual(7, new IntRange(3, 10).Length); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Features/SkeletonTypes.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System; 3 | 4 | namespace SourceAFIS.Engine.Features 5 | { 6 | static class SkeletonTypes 7 | { 8 | public static string Prefix(this SkeletonType type) 9 | { 10 | return type switch 11 | { 12 | SkeletonType.Ridges => "ridges-", 13 | SkeletonType.Valleys => "valleys-", 14 | _ => throw new ArgumentOutOfRangeException(nameof(type)), 15 | }; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Primitives/IntRange.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | 3 | namespace SourceAFIS.Engine.Primitives 4 | { 5 | readonly struct IntRange 6 | { 7 | public static readonly IntRange Zero = new IntRange(); 8 | public readonly int Start; 9 | public readonly int End; 10 | 11 | public int Length => End - Start; 12 | 13 | public IntRange(int start, int end) 14 | { 15 | Start = start; 16 | End = end; 17 | } 18 | 19 | public override string ToString() => $"{Start}..{End}"; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Extractor/Skeletons/SkeletonDotFilter.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System.Collections.Generic; 3 | using SourceAFIS.Engine.Features; 4 | 5 | namespace SourceAFIS.Engine.Extractor.Skeletons 6 | { 7 | static class SkeletonDotFilter 8 | { 9 | public static void Apply(Skeleton skeleton) 10 | { 11 | var removed = new List(); 12 | foreach (var minutia in skeleton.Minutiae) 13 | if (minutia.Ridges.Count == 0) 14 | removed.Add(minutia); 15 | foreach (var minutia in removed) 16 | skeleton.RemoveMinutia(minutia); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Extractor/Minutiae/InnerMinutiaeFilter.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System.Collections.Generic; 3 | using SourceAFIS.Engine.Configuration; 4 | using SourceAFIS.Engine.Features; 5 | using SourceAFIS.Engine.Primitives; 6 | 7 | namespace SourceAFIS.Engine.Extractor.Minutiae 8 | { 9 | static class InnerMinutiaeFilter 10 | { 11 | public static void Apply(List minutiae, BooleanMatrix mask) 12 | { 13 | minutiae.RemoveAll(minutia => 14 | { 15 | var arrow = (-Parameters.MaskDisplacement * DoubleAngle.ToVector(minutia.Direction)).Round(); 16 | return !mask.Get(minutia.Position.ToInt() + arrow, false); 17 | }); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Extractor/Skeletons/SkeletonFilters.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using SourceAFIS.Engine.Features; 3 | 4 | namespace SourceAFIS.Engine.Extractor.Skeletons 5 | { 6 | static class SkeletonFilters 7 | { 8 | public static void Apply(Skeleton skeleton) 9 | { 10 | SkeletonDotFilter.Apply(skeleton); 11 | // https://sourceafis.machinezoo.com/transparency/removed-dots 12 | FingerprintTransparency.Current.LogSkeleton("removed-dots", skeleton); 13 | SkeletonPoreFilter.Apply(skeleton); 14 | SkeletonGapFilter.Apply(skeleton); 15 | SkeletonTailFilter.Apply(skeleton); 16 | SkeletonFragmentFilter.Apply(skeleton); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Extractor/Skeletons/SkeletonGraphs.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using SourceAFIS.Engine.Features; 3 | using SourceAFIS.Engine.Primitives; 4 | 5 | namespace SourceAFIS.Engine.Extractor.Skeletons 6 | { 7 | static class SkeletonGraphs 8 | { 9 | public static Skeleton Create(BooleanMatrix binary, SkeletonType type) 10 | { 11 | // https://sourceafis.machinezoo.com/transparency/binarized-skeleton 12 | FingerprintTransparency.Current.Log(type.Prefix() + "binarized-skeleton", binary); 13 | var thinned = BinaryThinning.Thin(binary, type); 14 | var skeleton = SkeletonTracing.Trace(thinned, type); 15 | SkeletonFilters.Apply(skeleton); 16 | return skeleton; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Extractor/Minutiae/MinutiaCloudFilter.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using SourceAFIS.Engine.Configuration; 5 | using SourceAFIS.Engine.Features; 6 | using SourceAFIS.Engine.Primitives; 7 | 8 | namespace SourceAFIS.Engine.Extractor.Minutiae 9 | { 10 | static class MinutiaCloudFilter 11 | { 12 | public static void Apply(List minutiae) 13 | { 14 | var radiusSq = Integers.Sq(Parameters.MinutiaCloudRadius); 15 | var kept = minutiae.Where(m => Parameters.MaxCloudSize >= minutiae.Where(n => (n.Position - m.Position).LengthSq <= radiusSq).Count() - 1).ToList(); 16 | minutiae.Clear(); 17 | minutiae.AddRange(kept); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Transparency/ConsistentPairingGraph.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using SourceAFIS.Engine.Matcher; 5 | 6 | namespace SourceAFIS.Engine.Transparency 7 | { 8 | record ConsistentPairingGraph(ConsistentMinutiaPair Root, List Tree, List Support) 9 | { 10 | public ConsistentPairingGraph(int count, MinutiaPair[] pairs, List support) 11 | : this( 12 | new ConsistentMinutiaPair(pairs[0].Probe, pairs[0].Candidate), 13 | (from p in pairs select new ConsistentEdgePair(p)).Take(count).ToList(), 14 | (from p in support select new ConsistentEdgePair(p)).ToList()) 15 | { 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Extractor/AbsoluteContrastMask.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using SourceAFIS.Engine.Configuration; 3 | using SourceAFIS.Engine.Primitives; 4 | 5 | namespace SourceAFIS.Engine.Extractor 6 | { 7 | static class AbsoluteContrastMask 8 | { 9 | public static BooleanMatrix Compute(DoubleMatrix contrast) 10 | { 11 | var result = new BooleanMatrix(contrast.Size); 12 | foreach (var block in contrast.Size.Iterate()) 13 | if (contrast[block] < Parameters.MinAbsoluteContrast) 14 | result[block] = true; 15 | // https://sourceafis.machinezoo.com/transparency/absolute-contrast-mask 16 | FingerprintTransparency.Current.Log("absolute-contrast-mask", result); 17 | return result; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Extractor/Skeletons/SkeletonGap.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System; 3 | using SourceAFIS.Engine.Features; 4 | 5 | namespace SourceAFIS.Engine.Extractor.Skeletons 6 | { 7 | class SkeletonGap : IComparable 8 | { 9 | public int Distance; 10 | public SkeletonMinutia End1; 11 | public SkeletonMinutia End2; 12 | public int CompareTo(SkeletonGap other) 13 | { 14 | int distanceCmp = Distance.CompareTo(other.Distance); 15 | if (distanceCmp != 0) 16 | return distanceCmp; 17 | int end1Cmp = End1.Position.CompareTo(other.End1.Position); 18 | if (end1Cmp != 0) 19 | return end1Cmp; 20 | return End2.Position.CompareTo(other.End2.Position); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Features/IndexedEdge.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System.Runtime.InteropServices; 3 | 4 | namespace SourceAFIS.Engine.Features 5 | { 6 | // Explicitly request sequential layout for predictable memory usage. 7 | [StructLayout(LayoutKind.Sequential)] 8 | readonly struct IndexedEdge 9 | { 10 | // Mind the field order. Let the floats in shape get aligned in the whole 16-byte structure. 11 | public readonly EdgeShape Shape; 12 | public readonly byte Reference; 13 | public readonly byte Neighbor; 14 | 15 | public IndexedEdge(Minutia[] minutiae, int reference, int neighbor) 16 | { 17 | Shape = new(minutiae[reference], minutiae[neighbor]); 18 | Reference = (byte)reference; 19 | Neighbor = (byte)neighbor; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /SourceAFIS/FingerprintCompatibility.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | 3 | // TODO: Port FingerprintIO from Java. 4 | namespace SourceAFIS 5 | { 6 | /// Collection of methods helping with template compatibility. 7 | public static class FingerprintCompatibility 8 | { 9 | /// Version of the currently running SourceAFIS. 10 | /// Semantic version in a three-part 1.2.3 format. 11 | /// 12 | /// This is useful during upgrades when the application has to deal 13 | /// with possible template incompatibility between versions. 14 | /// Versions of different language ports of SourceAFIS are not kept in sync. 15 | /// 16 | public static string Version => typeof(FingerprintCompatibility).Assembly.GetName().Version.ToString(3); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /SourceAFIS.Tests/SourceAFIS.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0 4 | 12.0 5 | true 6 | false 7 | SourceAFIS 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Extractor/Minutiae/MinutiaCollector.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System.Collections.Generic; 3 | using SourceAFIS.Engine.Features; 4 | 5 | namespace SourceAFIS.Engine.Extractor.Minutiae 6 | { 7 | static class MinutiaCollector 8 | { 9 | static void Collect(List minutiae, Skeleton skeleton, MinutiaType type) 10 | { 11 | foreach (var sminutia in skeleton.Minutiae) 12 | if (sminutia.Ridges.Count == 1) 13 | minutiae.Add(new(sminutia.Position.ToShort(), sminutia.Ridges[0].Direction(), type)); 14 | } 15 | public static List Collect(Skeleton ridges, Skeleton valleys) 16 | { 17 | var minutiae = new List(); 18 | Collect(minutiae, ridges, MinutiaType.Ending); 19 | Collect(minutiae, valleys, MinutiaType.Bifurcation); 20 | return minutiae; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Primitives/Doubles.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System; 3 | 4 | namespace SourceAFIS.Engine.Primitives 5 | { 6 | static class Doubles 7 | { 8 | public static int RoundToInt(double value) => (int)Math.Floor(value + 0.5); 9 | public static double Sq(double value) => value * value; 10 | public static double Interpolate(double start, double end, double position) => start + position * (end - start); 11 | public static double Interpolate(double bottomleft, double bottomright, double topleft, double topright, double x, double y) 12 | { 13 | double left = Interpolate(topleft, bottomleft, y); 14 | double right = Interpolate(topright, bottomright, y); 15 | return Interpolate(left, right, x); 16 | } 17 | public static double InterpolateExponential(double start, double end, double position) => Math.Pow(end / start, position) * start; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Extractor/Skeletons/SkeletonFragmentFilter.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using SourceAFIS.Engine.Configuration; 3 | using SourceAFIS.Engine.Features; 4 | 5 | namespace SourceAFIS.Engine.Extractor.Skeletons 6 | { 7 | static class SkeletonFragmentFilter 8 | { 9 | public static void Apply(Skeleton skeleton) 10 | { 11 | foreach (var minutia in skeleton.Minutiae) 12 | if (minutia.Ridges.Count == 1) 13 | { 14 | var ridge = minutia.Ridges[0]; 15 | if (ridge.End.Ridges.Count == 1 && ridge.Points.Count < Parameters.MinFragmentLength) 16 | ridge.Detach(); 17 | } 18 | SkeletonDotFilter.Apply(skeleton); 19 | // https://sourceafis.machinezoo.com/transparency/removed-fragments 20 | FingerprintTransparency.Current.LogSkeleton("removed-fragments", skeleton); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Extractor/Skeletons/SkeletonTailFilter.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using SourceAFIS.Engine.Configuration; 3 | using SourceAFIS.Engine.Features; 4 | 5 | namespace SourceAFIS.Engine.Extractor.Skeletons 6 | { 7 | static class SkeletonTailFilter 8 | { 9 | public static void Apply(Skeleton skeleton) 10 | { 11 | foreach (var minutia in skeleton.Minutiae) 12 | { 13 | if (minutia.Ridges.Count == 1 && minutia.Ridges[0].End.Ridges.Count >= 3) 14 | if (minutia.Ridges[0].Points.Count < Parameters.MinTailLength) 15 | minutia.Ridges[0].Detach(); 16 | } 17 | SkeletonDotFilter.Apply(skeleton); 18 | SkeletonKnotFilter.Apply(skeleton); 19 | // https://sourceafis.machinezoo.com/transparency/removed-tails 20 | FingerprintTransparency.Current.LogSkeleton("removed-tails", skeleton); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Matcher/RootList.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System.Collections.Generic; 3 | using SourceAFIS.Engine.Configuration; 4 | 5 | namespace SourceAFIS.Engine.Matcher 6 | { 7 | class RootList 8 | { 9 | public readonly MinutiaPairPool Pool; 10 | public int Count; 11 | public readonly MinutiaPair[] Pairs = new MinutiaPair[Parameters.MaxTriedRoots]; 12 | public readonly HashSet Duplicates = new HashSet(); 13 | public RootList(MinutiaPairPool pool) => Pool = pool; 14 | public void Add(MinutiaPair pair) 15 | { 16 | Pairs[Count] = pair; 17 | ++Count; 18 | } 19 | public void Discard() 20 | { 21 | for (int i = 0; i < Count; ++i) 22 | { 23 | Pool.Release(Pairs[i]); 24 | Pairs[i] = null; 25 | } 26 | Count = 0; 27 | Duplicates.Clear(); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Features/SkeletonMinutia.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System.Collections.Generic; 3 | using SourceAFIS.Engine.Primitives; 4 | 5 | namespace SourceAFIS.Engine.Features 6 | { 7 | class SkeletonMinutia 8 | { 9 | public readonly IntPoint Position; 10 | public readonly List Ridges = new List(); 11 | 12 | public SkeletonMinutia(IntPoint position) => Position = position; 13 | 14 | public void AttachStart(SkeletonRidge ridge) 15 | { 16 | if (!Ridges.Contains(ridge)) 17 | { 18 | Ridges.Add(ridge); 19 | ridge.Start = this; 20 | } 21 | } 22 | public void DetachStart(SkeletonRidge ridge) 23 | { 24 | if (Ridges.Contains(ridge)) 25 | { 26 | Ridges.Remove(ridge); 27 | if (ridge.Start == this) 28 | ridge.Start = null; 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Primitives/IntMatrix.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | 3 | namespace SourceAFIS.Engine.Primitives 4 | { 5 | class IntMatrix 6 | { 7 | public readonly int Width; 8 | public readonly int Height; 9 | readonly int[] cells; 10 | 11 | public IntPoint Size => new IntPoint(Width, Height); 12 | 13 | public IntMatrix(int width, int height) 14 | { 15 | Width = width; 16 | Height = height; 17 | cells = new int[width * height]; 18 | } 19 | public IntMatrix(IntPoint size) : this(size.X, size.Y) { } 20 | 21 | public int this[int x, int y] 22 | { 23 | get => cells[Offset(x, y)]; 24 | set => cells[Offset(x, y)] = value; 25 | } 26 | public int this[IntPoint at] 27 | { 28 | get => this[at.X, at.Y]; 29 | set => this[at.X, at.Y] = value; 30 | } 31 | 32 | int Offset(int x, int y) => y * Width + x; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Primitives/BlockGrid.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | 3 | namespace SourceAFIS.Engine.Primitives 4 | { 5 | class BlockGrid 6 | { 7 | public readonly IntPoint Blocks; 8 | public readonly IntPoint Corners; 9 | public readonly int[] X; 10 | public readonly int[] Y; 11 | 12 | public BlockGrid(IntPoint size) 13 | { 14 | Blocks = size; 15 | Corners = new IntPoint(size.X + 1, size.Y + 1); 16 | X = new int[size.X + 1]; 17 | Y = new int[size.Y + 1]; 18 | } 19 | public BlockGrid(int width, int height) : this(new IntPoint(width, height)) { } 20 | 21 | public IntPoint Corner(int atX, int atY) => new IntPoint(X[atX], Y[atY]); 22 | public IntPoint Corner(IntPoint at) => Corner(at.X, at.Y); 23 | public IntRect Block(int atX, int atY) => IntRect.Between(Corner(atX, atY), Corner(atX + 1, atY + 1)); 24 | public IntRect Block(IntPoint at) => Block(at.X, at.Y); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Extractor/Minutiae/TopMinutiaeFilter.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using SourceAFIS.Engine.Configuration; 5 | using SourceAFIS.Engine.Features; 6 | 7 | namespace SourceAFIS.Engine.Extractor.Minutiae 8 | { 9 | static class TopMinutiaeFilter 10 | { 11 | public static List Apply(List minutiae) 12 | { 13 | if (minutiae.Count <= Parameters.MaxMinutiae) 14 | return minutiae; 15 | return 16 | (from minutia in minutiae 17 | let radiusSq = (from neighbor in minutiae 18 | let distanceSq = (minutia.Position - neighbor.Position).LengthSq 19 | orderby distanceSq 20 | select distanceSq).Skip(Parameters.SortByNeighbor).First() 21 | orderby radiusSq descending 22 | select minutia).Take(Parameters.MaxMinutiae).ToList(); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Matcher/MinutiaPairPool.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System; 3 | 4 | namespace SourceAFIS.Engine.Matcher 5 | { 6 | class MinutiaPairPool 7 | { 8 | MinutiaPair[] pool = new MinutiaPair[1]; 9 | int pooled; 10 | public MinutiaPair Allocate() 11 | { 12 | if (pooled > 0) 13 | { 14 | --pooled; 15 | var pair = pool[pooled]; 16 | pool[pooled] = null; 17 | return pair; 18 | } 19 | else 20 | return new MinutiaPair(); 21 | } 22 | public void Release(MinutiaPair pair) 23 | { 24 | if (pooled >= pool.Length) 25 | Array.Resize(ref pool, 2 * pool.Length); 26 | pair.Probe = 0; 27 | pair.Candidate = 0; 28 | pair.ProbeRef = 0; 29 | pair.CandidateRef = 0; 30 | pair.Distance = 0; 31 | pair.SupportingEdges = 0; 32 | pool[pooled] = pair; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Matcher/ScoringData.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | 3 | namespace SourceAFIS.Engine.Matcher 4 | { 5 | class ScoringData 6 | { 7 | public int MinutiaCount; 8 | public double MinutiaScore; 9 | public double MinutiaFractionInProbe; 10 | public double MinutiaFractionInCandidate; 11 | public double MinutiaFraction; 12 | public double MinutiaFractionScore; 13 | public int SupportingEdgeSum; 14 | public int EdgeCount; 15 | public double EdgeScore; 16 | public int SupportedMinutiaCount; 17 | public double SupportedMinutiaScore; 18 | public int MinutiaTypeHits; 19 | public double MinutiaTypeScore; 20 | public int DistanceErrorSum; 21 | public int DistanceAccuracySum; 22 | public double DistanceAccuracyScore; 23 | public float AngleErrorSum; 24 | public float AngleAccuracySum; 25 | public double AngleAccuracyScore; 26 | public double TotalScore; 27 | public double ShapedScore; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Features/Minutia.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System.Runtime.InteropServices; 3 | using SourceAFIS.Engine.Primitives; 4 | 5 | namespace SourceAFIS.Engine.Features 6 | { 7 | // Explicitly request sequential layout for predictable memory usage. 8 | // Do not pack the struct, so that the float field remains aligned in minutia arrays. 9 | [StructLayout(LayoutKind.Sequential)] 10 | readonly struct Minutia 11 | { 12 | // Mind the field order. Let the point and float take aligned positions. 13 | public readonly ShortPoint Position; 14 | public readonly float Direction; 15 | public readonly MinutiaType Type; 16 | 17 | // Struct alignment will force padding after type field. 18 | public const int Memory = ShortPoint.Memory + sizeof(float) + sizeof(float); 19 | 20 | public Minutia(ShortPoint position, float direction, MinutiaType type) 21 | { 22 | Position = position; 23 | Direction = direction; 24 | Type = type; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute to SourceAFIS for .NET 2 | 3 | Thank you for taking interest in SourceAFIS for .NET. 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 of them. 8 | 9 | - [sourceafis-net @ GitHub](https://github.com/robertvazan/sourceafis-net) 10 | - [sourceafis-net @ Bitbucket](https://bitbucket.org/robertvazan/sourceafis-net) 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, please open an issue first to discuss your idea. 19 | 20 | Don't worry too much about formatting and naming. Code will be reformatted after merge. Just avoid running your formatter on whole source files, because that makes diffs hard to understand. 21 | 22 | ## License 23 | 24 | Your submissions will be distributed under the project's [Apache License 2.0](LICENSE). 25 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Transparency/ConsistentSkeleton.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using SourceAFIS.Engine.Features; 5 | using SourceAFIS.Engine.Primitives; 6 | 7 | namespace SourceAFIS.Engine.Transparency 8 | { 9 | record ConsistentSkeleton(int Width, int Height, List Minutiae, List Ridges) 10 | { 11 | public static ConsistentSkeleton Of(Skeleton skeleton) 12 | { 13 | var offsets = new Dictionary(); 14 | for (int i = 0; i < skeleton.Minutiae.Count; ++i) 15 | offsets[skeleton.Minutiae[i]] = i; 16 | return new( 17 | skeleton.Size.X, 18 | skeleton.Size.Y, 19 | (from m in skeleton.Minutiae select m.Position).ToList(), 20 | (from m in skeleton.Minutiae 21 | from r in m.Ridges 22 | where r.Points is CircularList 23 | select new ConsistentSkeletonRidge(offsets[r.Start], offsets[r.End], r.Points)).ToList()); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /SourceAFIS.Tests/Resources/TestResources.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System; 3 | 4 | namespace SourceAFIS 5 | { 6 | static class TestResources 7 | { 8 | static byte[] Load(String name) 9 | { 10 | using (var stream = typeof(TestResources).Assembly.GetManifestResourceStream($"SourceAFIS.Resources.{name}")) 11 | { 12 | var data = new byte[stream.Length]; 13 | stream.Read(data, 0, data.Length); 14 | return data; 15 | } 16 | } 17 | public static byte[] Png() => Load("probe.png"); 18 | public static byte[] Jpeg() => Load("probe.jpeg"); 19 | public static byte[] Bmp() => Load("probe.bmp"); 20 | public static byte[] Probe() => Load("probe.png"); 21 | public static byte[] Matching() => Load("matching.png"); 22 | public static byte[] Nonmatching() => Load("nonmatching.png"); 23 | public static byte[] ProbeGray() => Load("gray-probe.dat"); 24 | public static byte[] MatchingGray() => Load("gray-matching.dat"); 25 | public static byte[] NonmatchingGray() => Load("gray-nonmatching.dat"); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Matcher/MatcherThread.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System; 3 | using System.Collections.Generic; 4 | using SourceAFIS.Engine.Primitives; 5 | 6 | namespace SourceAFIS.Engine.Matcher 7 | { 8 | class MatcherThread 9 | { 10 | [ThreadStatic] 11 | static MatcherThread current; 12 | public readonly MinutiaPairPool Pool = new MinutiaPairPool(); 13 | public readonly RootList Roots; 14 | public readonly PairingGraph Pairing; 15 | public PriorityQueue Queue = new PriorityQueue(Comparer.Create((a, b) => a.Distance.CompareTo(b.Distance))); 16 | public ScoringData Score = new ScoringData(); 17 | public static MatcherThread Current 18 | { 19 | get 20 | { 21 | if (current == null) 22 | current = new MatcherThread(); 23 | return current; 24 | } 25 | } 26 | public MatcherThread() 27 | { 28 | Roots = new RootList(Pool); 29 | Pairing = new PairingGraph(Pool); 30 | } 31 | public static void Kill() => current = null; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /SourceAFIS.Tests/FingerprintMatcherTest.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using NUnit.Framework; 3 | 4 | namespace SourceAFIS 5 | { 6 | public class FingerprintMatcherTest 7 | { 8 | void Matching(FingerprintTemplate probe, FingerprintTemplate candidate) 9 | { 10 | double score = new FingerprintMatcher(probe) 11 | .Match(candidate); 12 | Assert.Greater(score, 40); 13 | } 14 | void Nonmatching(FingerprintTemplate probe, FingerprintTemplate candidate) 15 | { 16 | double score = new FingerprintMatcher(probe) 17 | .Match(candidate); 18 | Assert.Less(score, 20); 19 | } 20 | [Test] public void MatchingPair() => Matching(FingerprintTemplateTest.Probe(), FingerprintTemplateTest.Matching()); 21 | [Test] public void NonmatchingPair() => Nonmatching(FingerprintTemplateTest.Probe(), FingerprintTemplateTest.Nonmatching()); 22 | [Test] public void MatchingGray() => Matching(FingerprintTemplateTest.ProbeGray(), FingerprintTemplateTest.MatchingGray()); 23 | [Test] public void NonmatchingGray() => Nonmatching(FingerprintTemplateTest.ProbeGray(), FingerprintTemplateTest.NonmatchingGray()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /SourceAFIS.Tests/Engine/Primitives/DoublePointTest.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using NUnit.Framework; 3 | 4 | namespace SourceAFIS.Engine.Primitives 5 | { 6 | public class DoublePointTest 7 | { 8 | [Test] 9 | public void Constructor() 10 | { 11 | var p = new DoublePoint(2.5, 3.5); 12 | Assert.AreEqual(2.5, p.X, 0.001); 13 | Assert.AreEqual(3.5, p.Y, 0.001); 14 | } 15 | [Test] 16 | public void Plus() => AssertPointEquals(new DoublePoint(6, 8), new DoublePoint(2, 3) + new DoublePoint(4, 5), 0.001); 17 | [Test] 18 | public void Multiply() => AssertPointEquals(new DoublePoint(1, 1.5), 0.5 * new DoublePoint(2, 3), 0.001); 19 | [Test] 20 | public void Round() 21 | { 22 | Assert.AreEqual(new IntPoint(2, 3), new DoublePoint(2.4, 2.6).Round()); 23 | Assert.AreEqual(new IntPoint(-2, -3), new DoublePoint(-2.4, -2.6).Round()); 24 | } 25 | internal static void AssertPointEquals(DoublePoint expected, DoublePoint actual, double tolerance) 26 | { 27 | Assert.AreEqual(expected.X, actual.X, tolerance); 28 | Assert.AreEqual(expected.Y, actual.Y, tolerance); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Primitives/DoublePoint.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | 3 | namespace SourceAFIS.Engine.Primitives 4 | { 5 | readonly struct DoublePoint 6 | { 7 | public static readonly DoublePoint Zero = new DoublePoint(); 8 | 9 | public readonly double X; 10 | public readonly double Y; 11 | 12 | public DoublePoint(double x, double y) 13 | { 14 | X = x; 15 | Y = y; 16 | } 17 | 18 | public static implicit operator DoublePoint(IntPoint point) => new DoublePoint(point.X, point.Y); 19 | public static DoublePoint operator +(DoublePoint left, DoublePoint right) => new DoublePoint(left.X + right.X, left.Y + right.Y); 20 | public static DoublePoint operator -(DoublePoint left, DoublePoint right) => new DoublePoint(left.X - right.X, left.Y - right.Y); 21 | public static DoublePoint operator -(DoublePoint point) => new DoublePoint(-point.X, -point.Y); 22 | public static DoublePoint operator *(double factor, DoublePoint point) => new DoublePoint(factor * point.X, factor * point.Y); 23 | 24 | public override string ToString() => $"[{X},{Y}]"; 25 | public IntPoint Round() => new IntPoint(Doubles.RoundToInt(X), Doubles.RoundToInt(Y)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Features/Skeleton.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System.Collections.Generic; 3 | using SourceAFIS.Engine.Primitives; 4 | 5 | namespace SourceAFIS.Engine.Features 6 | { 7 | partial class Skeleton 8 | { 9 | public readonly SkeletonType Type; 10 | public readonly IntPoint Size; 11 | public readonly List Minutiae = new List(); 12 | 13 | public Skeleton(SkeletonType type, IntPoint size) 14 | { 15 | Type = type; 16 | Size = size; 17 | } 18 | 19 | public void AddMinutia(SkeletonMinutia minutia) => Minutiae.Add(minutia); 20 | public void RemoveMinutia(SkeletonMinutia minutia) => Minutiae.Remove(minutia); 21 | public BooleanMatrix Shadow() 22 | { 23 | var shadow = new BooleanMatrix(Size); 24 | foreach (var minutia in Minutiae) 25 | { 26 | shadow[minutia.Position] = true; 27 | foreach (var ridge in minutia.Ridges) 28 | if (ridge.Start.Position.Y <= ridge.End.Position.Y) 29 | foreach (var point in ridge.Points) 30 | shadow[point] = true; 31 | } 32 | return shadow; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Primitives/FloatAngle.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System; 3 | 4 | namespace SourceAFIS.Engine.Primitives 5 | { 6 | static class FloatAngle 7 | { 8 | public const float Pi = (float)Math.PI; 9 | public const float Pi2 = (float)(2 * Math.PI); 10 | public const float HalfPi = (float)(0.5 * Math.PI); 11 | 12 | public static float Add(float start, float delta) 13 | { 14 | float angle = start + delta; 15 | return angle < Pi2 ? angle : angle - Pi2; 16 | } 17 | public static float Opposite(float angle) => angle < Pi ? angle + Pi : angle - Pi; 18 | public static float Distance(float first, float second) 19 | { 20 | float delta = Math.Abs(first - second); 21 | return delta <= Pi ? delta : Pi2 - delta; 22 | } 23 | public static float Difference(float first, float second) 24 | { 25 | float angle = first - second; 26 | return angle >= 0 ? angle : angle + Pi2; 27 | } 28 | public static float Complementary(float angle) 29 | { 30 | float complement = Pi2 - angle; 31 | return complement < Pi2 ? complement : complement - Pi2; 32 | } 33 | public static bool Normalized(float angle) => angle >= 0 && angle < Pi2; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /SourceAFIS.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | Project("{c2089935-fa78-511c-9971-4a28c3117e4f}") = "SourceAFIS", "SourceAFIS/SourceAFIS.csproj", "{c2089935-fa78-511c-9971-4a28c3117e4f}" 3 | EndProject 4 | Project("{987abc63-f974-59a2-bca4-ec4ec4f57dd2}") = "SourceAFIS.Tests", "SourceAFIS.Tests/SourceAFIS.Tests.csproj", "{987abc63-f974-59a2-bca4-ec4ec4f57dd2}" 5 | EndProject 6 | Global 7 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 8 | Debug|Any CPU = Debug|Any CPU 9 | Release|Any CPU = Release|Any CPU 10 | EndGlobalSection 11 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 12 | {c2089935-fa78-511c-9971-4a28c3117e4f}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 13 | {c2089935-fa78-511c-9971-4a28c3117e4f}.Debug|Any CPU.Build.0 = Debug|Any CPU 14 | {c2089935-fa78-511c-9971-4a28c3117e4f}.Release|Any CPU.ActiveCfg = Release|Any CPU 15 | {c2089935-fa78-511c-9971-4a28c3117e4f}.Release|Any CPU.Build.0 = Release|Any CPU 16 | {987abc63-f974-59a2-bca4-ec4ec4f57dd2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {987abc63-f974-59a2-bca4-ec4ec4f57dd2}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {987abc63-f974-59a2-bca4-ec4ec4f57dd2}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {987abc63-f974-59a2-bca4-ec4ec4f57dd2}.Release|Any CPU.Build.0 = Release|Any CPU 20 | EndGlobalSection 21 | EndGlobal 22 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Primitives/DoubleMatrix.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | 3 | namespace SourceAFIS.Engine.Primitives 4 | { 5 | class DoubleMatrix 6 | { 7 | public readonly int Width; 8 | public readonly int Height; 9 | readonly double[] cells; 10 | 11 | public IntPoint Size => new IntPoint(Width, Height); 12 | 13 | public DoubleMatrix(int width, int height) 14 | { 15 | Width = width; 16 | Height = height; 17 | cells = new double[width * height]; 18 | } 19 | public DoubleMatrix(IntPoint size) : this(size.X, size.Y) { } 20 | 21 | public double this[int x, int y] 22 | { 23 | get => cells[Offset(x, y)]; 24 | set => cells[Offset(x, y)] = value; 25 | } 26 | public double this[IntPoint at] 27 | { 28 | get => this[at.X, at.Y]; 29 | set => this[at.X, at.Y] = value; 30 | } 31 | 32 | public void Add(int x, int y, double value) => cells[Offset(x, y)] += value; 33 | public void Add(IntPoint at, double value) => Add(at.X, at.Y, value); 34 | public void Multiply(int x, int y, double value) => cells[Offset(x, y)] *= value; 35 | public void Multiply(IntPoint at, double value) => Multiply(at.X, at.Y, value); 36 | int Offset(int x, int y) => y * Width + x; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Primitives/BlockMap.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | 3 | namespace SourceAFIS.Engine.Primitives 4 | { 5 | class BlockMap 6 | { 7 | public readonly IntPoint Pixels; 8 | public readonly BlockGrid Primary; 9 | public readonly BlockGrid Secondary; 10 | public BlockMap(int width, int height, int maxBlockSize) 11 | { 12 | Pixels = new IntPoint(width, height); 13 | Primary = new BlockGrid(new IntPoint( 14 | Integers.RoundUpDiv(Pixels.X, maxBlockSize), 15 | Integers.RoundUpDiv(Pixels.Y, maxBlockSize))); 16 | for (int y = 0; y <= Primary.Blocks.Y; ++y) 17 | Primary.Y[y] = y * Pixels.Y / Primary.Blocks.Y; 18 | for (int x = 0; x <= Primary.Blocks.X; ++x) 19 | Primary.X[x] = x * Pixels.X / Primary.Blocks.X; 20 | Secondary = new BlockGrid(Primary.Corners); 21 | Secondary.Y[0] = 0; 22 | for (int y = 0; y < Primary.Blocks.Y; ++y) 23 | Secondary.Y[y + 1] = Primary.Block(0, y).Center.Y; 24 | Secondary.Y[Secondary.Blocks.Y] = Pixels.Y; 25 | Secondary.X[0] = 0; 26 | for (int x = 0; x < Primary.Blocks.X; ++x) 27 | Secondary.X[x + 1] = Primary.Block(x, 0).Center.X; 28 | Secondary.X[Secondary.Blocks.X] = Pixels.X; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Extractor/Skeletons/SkeletonKnotFilter.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using SourceAFIS.Engine.Features; 3 | 4 | namespace SourceAFIS.Engine.Extractor.Skeletons 5 | { 6 | static class SkeletonKnotFilter 7 | { 8 | public static void Apply(Skeleton skeleton) 9 | { 10 | foreach (var minutia in skeleton.Minutiae) 11 | { 12 | if (minutia.Ridges.Count == 2 && minutia.Ridges[0].Reversed != minutia.Ridges[1]) 13 | { 14 | var extended = minutia.Ridges[0].Reversed; 15 | var removed = minutia.Ridges[1]; 16 | if (extended.Points.Count < removed.Points.Count) 17 | { 18 | var tmp = extended; 19 | extended = removed; 20 | removed = tmp; 21 | extended = extended.Reversed; 22 | removed = removed.Reversed; 23 | } 24 | extended.Points.RemoveAt(extended.Points.Count - 1); 25 | foreach (var point in removed.Points) 26 | extended.Points.Add(point); 27 | extended.End = removed.End; 28 | removed.Detach(); 29 | } 30 | } 31 | SkeletonDotFilter.Apply(skeleton); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /SourceAFIS.Tests/Engine/Primitives/BlockGridTest.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using NUnit.Framework; 3 | 4 | namespace SourceAFIS.Engine.Primitives 5 | { 6 | public class BlockGridTest 7 | { 8 | BlockGrid g = new BlockGrid(3, 4); 9 | 10 | public BlockGridTest() 11 | { 12 | for (int i = 0; i < g.X.Length; ++i) 13 | g.X[i] = (i + 1) * 10; 14 | for (int i = 0; i < g.Y.Length; ++i) 15 | g.Y[i] = (i + 1) * 100; 16 | } 17 | 18 | [Test] 19 | public void Constructor() 20 | { 21 | Assert.AreEqual(4, g.X.Length); 22 | Assert.AreEqual(5, g.Y.Length); 23 | } 24 | [Test] 25 | public void ConstructorFromPoint() 26 | { 27 | var g = new BlockGrid(new IntPoint(2, 3)); 28 | Assert.AreEqual(3, g.X.Length); 29 | Assert.AreEqual(4, g.Y.Length); 30 | } 31 | [Test] 32 | public void CornerXY() => Assert.AreEqual(new IntPoint(20, 300), g.Corner(1, 2)); 33 | [Test] 34 | public void CornerAt() => Assert.AreEqual(new IntPoint(10, 200), g.Corner(new IntPoint(0, 1))); 35 | [Test] 36 | public void BlockXY() => Assert.AreEqual(new IntRect(20, 300, 10, 100), g.Block(1, 2)); 37 | [Test] 38 | public void BlockAt() => Assert.AreEqual(new IntRect(10, 200, 10, 100), g.Block(new IntPoint(0, 1))); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /SourceAFIS/SourceAFIS.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0 4 | 12.0 5 | true 6 | 3.14.0 7 | SourceAFIS for .NET 8 | robertvazan 9 | https://github.com/robertvazan/sourceafis-net 10 | https://sourceafis.machinezoo.com/net 11 | Fingerprint recognition engine that takes a pair of human fingerprint images and returns their similarity score. Supports efficient 1:N search. 12 | fingerprint; biometrics; authentication; sourceafis 13 | Apache-2.0 14 | README.md 15 | icon.png 16 | true 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /SourceAFIS.Tests/Engine/Primitives/BlockMapTest.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using NUnit.Framework; 3 | 4 | namespace SourceAFIS.Engine.Primitives 5 | { 6 | public class BlockMapTest 7 | { 8 | [Test] 9 | public void Constructor() 10 | { 11 | BlockMap m = new BlockMap(400, 600, 20); 12 | Assert.AreEqual(new IntPoint(400, 600), m.Pixels); 13 | Assert.AreEqual(new IntPoint(20, 30), m.Primary.Blocks); 14 | Assert.AreEqual(new IntPoint(21, 31), m.Primary.Corners); 15 | Assert.AreEqual(new IntPoint(21, 31), m.Secondary.Blocks); 16 | Assert.AreEqual(new IntPoint(22, 32), m.Secondary.Corners); 17 | Assert.AreEqual(new IntPoint(0, 0), m.Primary.Corner(0, 0)); 18 | Assert.AreEqual(new IntPoint(400, 600), m.Primary.Corner(20, 30)); 19 | Assert.AreEqual(new IntPoint(200, 300), m.Primary.Corner(10, 15)); 20 | Assert.AreEqual(new IntRect(0, 0, 20, 20), m.Primary.Block(0, 0)); 21 | Assert.AreEqual(new IntRect(380, 580, 20, 20), m.Primary.Block(19, 29)); 22 | Assert.AreEqual(new IntRect(200, 300, 20, 20), m.Primary.Block(10, 15)); 23 | Assert.AreEqual(new IntRect(0, 0, 10, 10), m.Secondary.Block(0, 0)); 24 | Assert.AreEqual(new IntRect(390, 590, 10, 10), m.Secondary.Block(20, 30)); 25 | Assert.AreEqual(new IntRect(190, 290, 20, 20), m.Secondary.Block(10, 15)); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Primitives/Integers.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | 3 | namespace SourceAFIS.Engine.Primitives 4 | { 5 | static class Integers 6 | { 7 | public static int Sq(int value) => value * value; 8 | public static int RoundUpDiv(int dividend, int divisor) => (dividend + divisor - 1) / divisor; 9 | // https://stackoverflow.com/questions/10439242/count-leading-zeroes-in-an-int32 10 | // Modified for unsigned values. 11 | // .NET Core 3 has BitOperations.PopCount() and BitOperations.LeadingZeroCount(). 12 | public static uint PopulationCount(uint x) 13 | { 14 | x -= x >> 1 & 0x55555555; 15 | x = (x >> 2 & 0x33333333) + (x & 0x33333333); 16 | x = (x >> 4) + x & 0x0f0f0f0f; 17 | x += x >> 8; 18 | x += x >> 16; 19 | return x & 0x0000003f; 20 | } 21 | public static uint LeadingZeros(uint x) 22 | { 23 | //compile time constant 24 | const uint numIntBits = sizeof(int) * 8; 25 | //do the smearing 26 | x |= x >> 1; 27 | x |= x >> 2; 28 | x |= x >> 4; 29 | x |= x >> 8; 30 | x |= x >> 16; 31 | //count the ones 32 | x -= x >> 1 & 0x55555555; 33 | x = (x >> 2 & 0x33333333) + (x & 0x33333333); 34 | x = (x >> 4) + x & 0x0f0f0f0f; 35 | x += x >> 8; 36 | x += x >> 16; 37 | return numIntBits - (x & 0x0000003f); //subtract # of 1s from 32 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /SourceAFIS/FingerprintImageOptions.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System; 3 | 4 | namespace SourceAFIS 5 | { 6 | /// Additional information about fingerprint image. 7 | /// 8 | /// FingerprintImageOptions can be passed to constructor 9 | /// to provide additional information about fingerprint image that supplements raw pixel data. 10 | /// Since SourceAFIS algorithm is not scale-invariant, all images should have DPI configured explicitly by setting . 11 | /// 12 | /// 13 | public class FingerprintImageOptions 14 | { 15 | double dpi = 500; 16 | 17 | /// Gets or sets image resolution. 18 | /// Image resolution in DPI (dots per inch), usually around 500. Default DPI is 500. 19 | /// 20 | /// SourceAFIS algorithm is not scale-invariant. Fingerprints with incorrectly configured DPI may fail to match. 21 | /// Check your fingerprint reader specification for correct DPI value. 22 | /// 23 | /// Thrown when DPI is non-positive, impossibly low, or impossibly high. 24 | public double Dpi 25 | { 26 | get => dpi; 27 | set 28 | { 29 | if (value < 20 || value > 20_000) 30 | throw new ArgumentOutOfRangeException(); 31 | dpi = value; 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![SWUbanner](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg)](https://github.com/vshymanskyy/StandWithUkraine/blob/main/docs/README.md) 2 | 3 | # SourceAFIS for .NET 4 | 5 | [![Build status](https://github.com/robertvazan/sourceafis-net/actions/workflows/build.yml/badge.svg)](https://github.com/robertvazan/sourceafis-net/actions/workflows/build.yml) 6 | [![Nuget](https://img.shields.io/nuget/v/SourceAFIS)](https://www.nuget.org/packages/SourceAFIS/) 7 | 8 | SourceAFIS for .NET is a pure C# port of [SourceAFIS](https://sourceafis.machinezoo.com/), 9 | an algorithm for recognition of human fingerprints. 10 | It can compare two fingerprints 1:1 or search a large database 1:N for matching fingerprint. 11 | It takes fingerprint images on input and produces similarity score on output. 12 | Similarity score is then compared to customizable match threshold. 13 | 14 | More on [homepage](https://sourceafis.machinezoo.com/net). 15 | 16 | ## Status 17 | 18 | Stable and maintained. 19 | 20 | ## Getting started 21 | 22 | See [homepage](https://sourceafis.machinezoo.com/net). 23 | 24 | ## Documentation 25 | 26 | * [SourceAFIS for .NET](https://sourceafis.machinezoo.com/net) 27 | * [XML doc comments](https://github.com/robertvazan/sourceafis-net/tree/master/SourceAFIS) 28 | * [SourceAFIS overview](https://sourceafis.machinezoo.com/) 29 | * [Algorithm](https://sourceafis.machinezoo.com/algorithm) 30 | 31 | ## Feedback 32 | 33 | Bug reports and pull requests are welcome. See [CONTRIBUTING.md](https://github.com/robertvazan/sourceafis-net/blob/master/CONTRIBUTING.md). 34 | 35 | ## License 36 | 37 | Distributed under [Apache License 2.0](https://github.com/robertvazan/sourceafis-net/blob/master/LICENSE). 38 | -------------------------------------------------------------------------------- /SourceAFIS.Tests/Engine/Primitives/IntegersTest.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using NUnit.Framework; 3 | 4 | namespace SourceAFIS.Engine.Primitives 5 | { 6 | public class IntegersTest 7 | { 8 | [Test] 9 | public void Sq() 10 | { 11 | Assert.AreEqual(9, Integers.Sq(3)); 12 | Assert.AreEqual(9, Integers.Sq(-3)); 13 | } 14 | [Test] 15 | public void RoundUpDiv() 16 | { 17 | Assert.AreEqual(3, Integers.RoundUpDiv(9, 3)); 18 | Assert.AreEqual(3, Integers.RoundUpDiv(8, 3)); 19 | Assert.AreEqual(3, Integers.RoundUpDiv(7, 3)); 20 | Assert.AreEqual(2, Integers.RoundUpDiv(6, 3)); 21 | Assert.AreEqual(5, Integers.RoundUpDiv(20, 4)); 22 | Assert.AreEqual(5, Integers.RoundUpDiv(19, 4)); 23 | Assert.AreEqual(5, Integers.RoundUpDiv(18, 4)); 24 | Assert.AreEqual(5, Integers.RoundUpDiv(17, 4)); 25 | Assert.AreEqual(4, Integers.RoundUpDiv(16, 4)); 26 | } 27 | [Test] 28 | public void PopulationCount() 29 | { 30 | Assert.AreEqual(0, Integers.PopulationCount(0)); 31 | Assert.AreEqual(1, Integers.PopulationCount(0x80)); 32 | Assert.AreEqual(2, Integers.PopulationCount(0x40008000)); 33 | Assert.AreEqual(3, Integers.PopulationCount(0x40208000)); 34 | } 35 | [Test] 36 | public void LeadingZeros() 37 | { 38 | Assert.AreEqual(32, Integers.LeadingZeros(0)); 39 | Assert.AreEqual(24, Integers.LeadingZeros(0x80)); 40 | Assert.AreEqual(1, Integers.LeadingZeros(0x40008000)); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /SourceAFIS.Tests/Engine/Primitives/SerializationUtilsTest.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using NUnit.Framework; 3 | using Dahomey.Cbor; 4 | 5 | namespace SourceAFIS.Engine.Primitives 6 | { 7 | public class SerializationUtilsTest 8 | { 9 | class BaseClass 10 | { 11 | public int BaseField = 1; 12 | } 13 | class TestClass : BaseClass 14 | { 15 | public int PublicField = 2; 16 | int PrivateField = 3; 17 | 18 | const int ConstField = 9; 19 | public int PublicProperty { get; set; } = 9; 20 | int PrivateProperty { get; set; } = 9; 21 | 22 | public int GetPrivate() => PrivateField; 23 | public void SetPrivate(int value) => PrivateField = value; 24 | } 25 | class ImmutableClass : TestClass 26 | { 27 | public readonly int ReadonlyField; 28 | 29 | public ImmutableClass(int value) => ReadonlyField = value; 30 | } 31 | 32 | [Test] 33 | public void Serialize() 34 | { 35 | byte[] cbor = SerializationUtils.Serialize(new ImmutableClass(4)); 36 | Assert.AreEqual("{\"baseField\":1,\"publicField\":2,\"privateField\":3,\"readonlyField\":4}", Cbor.ToJson(cbor)); 37 | } 38 | [Test] 39 | public void Deserialize() 40 | { 41 | var data = new TestClass(); 42 | data.PublicField = 777; 43 | data.SetPrivate(888); 44 | data = SerializationUtils.Deserialize(SerializationUtils.Serialize(data)); 45 | Assert.AreEqual(777, data.PublicField); 46 | Assert.AreEqual(888, data.GetPrivate()); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Extractor/RelativeContrastMask.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System; 3 | using System.Collections.Generic; 4 | using SourceAFIS.Engine.Configuration; 5 | using SourceAFIS.Engine.Primitives; 6 | 7 | namespace SourceAFIS.Engine.Extractor 8 | { 9 | static class RelativeContrastMask 10 | { 11 | public static BooleanMatrix Compute(DoubleMatrix contrast, BlockMap blocks) 12 | { 13 | var sortedContrast = new List(); 14 | foreach (var block in contrast.Size.Iterate()) 15 | sortedContrast.Add(contrast[block]); 16 | sortedContrast.Sort(); 17 | sortedContrast.Reverse(); 18 | int pixelsPerBlock = blocks.Pixels.Area / blocks.Primary.Blocks.Area; 19 | int sampleCount = Math.Min(sortedContrast.Count, Parameters.RelativeContrastSample / pixelsPerBlock); 20 | int consideredBlocks = Math.Max(Doubles.RoundToInt(sampleCount * Parameters.RelativeContrastPercentile), 1); 21 | double averageContrast = 0; 22 | for (int i = 0; i < consideredBlocks; ++i) 23 | averageContrast += sortedContrast[i]; 24 | averageContrast /= consideredBlocks; 25 | var limit = averageContrast * Parameters.MinRelativeContrast; 26 | var result = new BooleanMatrix(blocks.Primary.Blocks); 27 | foreach (var block in blocks.Primary.Blocks.Iterate()) 28 | if (contrast[block] < limit) 29 | result[block] = true; 30 | // https://sourceafis.machinezoo.com/transparency/relative-contrast-mask 31 | FingerprintTransparency.Current.Log("relative-contrast-mask", result); 32 | return result; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Primitives/HistogramCube.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System; 3 | 4 | namespace SourceAFIS.Engine.Primitives 5 | { 6 | class HistogramCube 7 | { 8 | public readonly int Width; 9 | public readonly int Height; 10 | public readonly int Bins; 11 | readonly int[] counts; 12 | 13 | public HistogramCube(int width, int height, int bins) 14 | { 15 | Width = width; 16 | Height = height; 17 | Bins = bins; 18 | counts = new int[width * height * bins]; 19 | } 20 | public HistogramCube(IntPoint size, int bins) : this(size.X, size.Y, bins) { } 21 | 22 | public int this[int x, int y, int z] 23 | { 24 | get => counts[Offset(x, y, z)]; 25 | set => counts[Offset(x, y, z)] = value; 26 | } 27 | public int this[IntPoint at, int z] 28 | { 29 | get => this[at.X, at.Y, z]; 30 | set => this[at.X, at.Y, z] = value; 31 | } 32 | 33 | public int Constrain(int z) => Math.Max(0, Math.Min(Bins - 1, z)); 34 | public int Sum(int x, int y) 35 | { 36 | int sum = 0; 37 | for (int i = 0; i < Bins; ++i) 38 | sum += this[x, y, i]; 39 | return sum; 40 | } 41 | public int Sum(IntPoint at) => Sum(at.X, at.Y); 42 | public void Add(int x, int y, int z, int value) => counts[Offset(x, y, z)] += value; 43 | public void Add(IntPoint at, int z, int value) => Add(at.X, at.Y, z, value); 44 | public void Increment(int x, int y, int z) => Add(x, y, z, 1); 45 | public void Increment(IntPoint at, int z) => Increment(at.X, at.Y, z); 46 | int Offset(int x, int y, int z) => (y * Width + x) * Bins + z; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Extractor/ClippedContrast.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using SourceAFIS.Engine.Configuration; 3 | using SourceAFIS.Engine.Primitives; 4 | 5 | namespace SourceAFIS.Engine.Extractor 6 | { 7 | static class ClippedContrast 8 | { 9 | public static DoubleMatrix Compute(BlockMap blocks, HistogramCube histogram) 10 | { 11 | var result = new DoubleMatrix(blocks.Primary.Blocks); 12 | foreach (var block in blocks.Primary.Blocks.Iterate()) 13 | { 14 | int volume = histogram.Sum(block); 15 | int clipLimit = Doubles.RoundToInt(volume * Parameters.ClippedContrast); 16 | int accumulator = 0; 17 | int lowerBound = histogram.Bins - 1; 18 | for (int i = 0; i < histogram.Bins; ++i) 19 | { 20 | accumulator += histogram[block, i]; 21 | if (accumulator > clipLimit) 22 | { 23 | lowerBound = i; 24 | break; 25 | } 26 | } 27 | accumulator = 0; 28 | int upperBound = 0; 29 | for (int i = histogram.Bins - 1; i >= 0; --i) 30 | { 31 | accumulator += histogram[block, i]; 32 | if (accumulator > clipLimit) 33 | { 34 | upperBound = i; 35 | break; 36 | } 37 | } 38 | result[block] = (upperBound - lowerBound) * (1.0 / (histogram.Bins - 1)); 39 | } 40 | // https://sourceafis.machinezoo.com/transparency/contrast 41 | FingerprintTransparency.Current.Log("contrast", result); 42 | return result; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Primitives/DoublePointMatrix.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | 3 | namespace SourceAFIS.Engine.Primitives 4 | { 5 | class DoublePointMatrix 6 | { 7 | public readonly int Width; 8 | public readonly int Height; 9 | readonly double[] vectors; 10 | 11 | public IntPoint Size => new IntPoint(Width, Height); 12 | 13 | public DoublePointMatrix(int width, int height) 14 | { 15 | Width = width; 16 | Height = height; 17 | vectors = new double[2 * width * height]; 18 | } 19 | public DoublePointMatrix(IntPoint size) : this(size.X, size.Y) { } 20 | 21 | public DoublePoint this[int x, int y] 22 | { 23 | get 24 | { 25 | int i = Offset(x, y); 26 | return new DoublePoint(vectors[i], vectors[i + 1]); 27 | } 28 | set 29 | { 30 | int i = Offset(x, y); 31 | vectors[i] = value.X; 32 | vectors[i + 1] = value.Y; 33 | } 34 | } 35 | public DoublePoint this[IntPoint at] 36 | { 37 | get => this[at.X, at.Y]; 38 | set => this[at.X, at.Y] = value; 39 | } 40 | 41 | public void Set(int x, int y, double px, double py) 42 | { 43 | int i = Offset(x, y); 44 | vectors[i] = px; 45 | vectors[i + 1] = py; 46 | } 47 | public void Add(int x, int y, double px, double py) 48 | { 49 | int i = Offset(x, y); 50 | vectors[i] += px; 51 | vectors[i + 1] += py; 52 | } 53 | public void Add(int x, int y, DoublePoint point) => Add(x, y, point.X, point.Y); 54 | public void Add(IntPoint at, DoublePoint point) => Add(at.X, at.Y, point); 55 | int Offset(int x, int y) => 2 * (y * Width + x); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Primitives/ReversedList.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System; 3 | using System.Collections; 4 | using System.Collections.Generic; 5 | 6 | namespace SourceAFIS.Engine.Primitives 7 | { 8 | class ReversedList : IList 9 | { 10 | IList inner; 11 | 12 | public int Count => inner.Count; 13 | public bool IsReadOnly => inner.IsReadOnly; 14 | public T this[int index] 15 | { 16 | get => inner[Count - index - 1]; 17 | set => inner[Count - index - 1] = value; 18 | } 19 | 20 | public ReversedList(IList inner) => this.inner = inner; 21 | 22 | public int IndexOf(T item) 23 | { 24 | for (int i = 0; i < Count; ++i) 25 | if (EqualityComparer.Default.Equals(this[i], item)) 26 | return i; 27 | return -1; 28 | } 29 | public void Insert(int position, T item) => inner.Insert(Count - position, item); 30 | public void RemoveAt(int position) => inner.RemoveAt(Count - position - 1); 31 | public void Add(T item) => inner.Insert(0, item); 32 | public void Clear() => inner.Clear(); 33 | public bool Contains(T item) => inner.Contains(item); 34 | public void CopyTo(T[] array, int at) 35 | { 36 | inner.CopyTo(array, at); 37 | Array.Reverse(array, at, Count); 38 | } 39 | public bool Remove(T item) 40 | { 41 | int index = IndexOf(item); 42 | if (index >= 0) 43 | { 44 | inner.RemoveAt(Count - index - 1); 45 | return true; 46 | } 47 | else 48 | return false; 49 | } 50 | 51 | IEnumerator IEnumerable.GetEnumerator() 52 | { 53 | for (int i = 0; i < Count; ++i) 54 | yield return this[i]; 55 | } 56 | IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)this).GetEnumerator(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Extractor/Skeletons/SkeletonPoreFilter.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using SourceAFIS.Engine.Configuration; 3 | using SourceAFIS.Engine.Features; 4 | 5 | namespace SourceAFIS.Engine.Extractor.Skeletons 6 | { 7 | static class SkeletonPoreFilter 8 | { 9 | public static void Apply(Skeleton skeleton) 10 | { 11 | foreach (var minutia in skeleton.Minutiae) 12 | { 13 | if (minutia.Ridges.Count == 3) 14 | { 15 | for (int exit = 0; exit < 3; ++exit) 16 | { 17 | var exitRidge = minutia.Ridges[exit]; 18 | var arm1 = minutia.Ridges[(exit + 1) % 3]; 19 | var arm2 = minutia.Ridges[(exit + 2) % 3]; 20 | if (arm1.End == arm2.End && exitRidge.End != arm1.End && arm1.End != minutia && exitRidge.End != minutia) 21 | { 22 | var end = arm1.End; 23 | if (end.Ridges.Count == 3 && arm1.Points.Count <= Parameters.MaxPoreArm && arm2.Points.Count <= Parameters.MaxPoreArm) 24 | { 25 | arm1.Detach(); 26 | arm2.Detach(); 27 | var merged = new SkeletonRidge(); 28 | merged.Start = minutia; 29 | merged.End = end; 30 | foreach (var point in minutia.Position.LineTo(end.Position)) 31 | merged.Points.Add(point); 32 | } 33 | break; 34 | } 35 | } 36 | } 37 | } 38 | SkeletonKnotFilter.Apply(skeleton); 39 | // https://sourceafis.machinezoo.com/transparency/removed-pores 40 | FingerprintTransparency.Current.LogSkeleton("removed-pores", skeleton); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Primitives/BooleanMatrix.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System; 3 | 4 | namespace SourceAFIS.Engine.Primitives 5 | { 6 | class BooleanMatrix 7 | { 8 | public readonly int Width; 9 | public readonly int Height; 10 | readonly bool[] cells; 11 | 12 | public IntPoint Size => new IntPoint(Width, Height); 13 | 14 | public BooleanMatrix(int width, int height) 15 | { 16 | Width = width; 17 | Height = height; 18 | cells = new bool[width * height]; 19 | } 20 | public BooleanMatrix(IntPoint size) : this(size.X, size.Y) { } 21 | public BooleanMatrix(BooleanMatrix other) 22 | : this(other.Size) 23 | { 24 | for (int i = 0; i < cells.Length; ++i) 25 | cells[i] = other.cells[i]; 26 | } 27 | 28 | public bool this[int x, int y] 29 | { 30 | get => cells[Offset(x, y)]; 31 | set => cells[Offset(x, y)] = value; 32 | } 33 | public bool this[IntPoint at] 34 | { 35 | get => this[at.X, at.Y]; 36 | set => this[at.X, at.Y] = value; 37 | } 38 | 39 | public bool Get(int x, int y, bool fallback) 40 | { 41 | if (x < 0 || y < 0 || x >= Width || y >= Height) 42 | return fallback; 43 | return cells[Offset(x, y)]; 44 | } 45 | public bool Get(IntPoint at, bool fallback) => Get(at.X, at.Y, fallback); 46 | public void Invert() 47 | { 48 | for (int i = 0; i < cells.Length; ++i) 49 | cells[i] = !cells[i]; 50 | } 51 | public void Merge(BooleanMatrix other) 52 | { 53 | if (other.Width != Width || other.Height != Height) 54 | throw new ArgumentException(); 55 | for (int i = 0; i < cells.Length; ++i) 56 | cells[i] |= other.cells[i]; 57 | } 58 | int Offset(int x, int y) => y * Width + x; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Primitives/ShortPoint.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System; 3 | using System.Runtime.InteropServices; 4 | 5 | namespace SourceAFIS.Engine.Primitives 6 | { 7 | // Explicitly request sequential layout for predictable memory usage. 8 | [StructLayout(LayoutKind.Sequential)] 9 | readonly struct ShortPoint : IEquatable, IComparable 10 | { 11 | public readonly short X; 12 | public readonly short Y; 13 | 14 | public const int Memory = 2 * sizeof(short); 15 | 16 | public int LengthSq => Integers.Sq(X) + Integers.Sq(Y); 17 | 18 | public ShortPoint(short x, short y) 19 | { 20 | X = x; 21 | Y = y; 22 | } 23 | public ShortPoint(int x, int y) 24 | { 25 | X = (short)x; 26 | Y = (short)y; 27 | } 28 | 29 | public static ShortPoint operator +(ShortPoint left, ShortPoint right) => new(left.X + right.X, left.Y + right.Y); 30 | public static ShortPoint operator -(ShortPoint left, ShortPoint right) => new(left.X - right.X, left.Y - right.Y); 31 | public static ShortPoint operator -(ShortPoint point) => new(-point.X, -point.Y); 32 | public static bool operator ==(ShortPoint left, ShortPoint right) => left.X == right.X && left.Y == right.Y; 33 | public static bool operator !=(ShortPoint left, ShortPoint right) => left.X != right.X || left.Y != right.Y; 34 | 35 | public override int GetHashCode() => 31 * X + Y; 36 | public bool Equals(ShortPoint other) => X == other.X && Y == other.Y; 37 | public override bool Equals(object other) => other is ShortPoint p && Equals(p); 38 | public int CompareTo(ShortPoint other) 39 | { 40 | int resultY = Y.CompareTo(other.Y); 41 | if (resultY != 0) 42 | return resultY; 43 | return X.CompareTo(other.X); 44 | } 45 | public override string ToString() => $"[{X},{Y}]"; 46 | public IntPoint ToInt() => new(X, Y); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Extractor/ImageResizer.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System; 3 | using SourceAFIS.Engine.Primitives; 4 | 5 | namespace SourceAFIS.Engine.Extractor 6 | { 7 | static class ImageResizer 8 | { 9 | static DoubleMatrix Resize(DoubleMatrix input, int newWidth, int newHeight) 10 | { 11 | if (newWidth == input.Width && newHeight == input.Height) 12 | return input; 13 | var output = new DoubleMatrix(newWidth, newHeight); 14 | double scaleX = newWidth / (double)input.Width; 15 | double scaleY = newHeight / (double)input.Height; 16 | double descaleX = 1 / scaleX; 17 | double descaleY = 1 / scaleY; 18 | for (int y = 0; y < newHeight; ++y) 19 | { 20 | double y1 = y * descaleY; 21 | double y2 = y1 + descaleY; 22 | int y1i = (int)y1; 23 | int y2i = Math.Min((int)Math.Ceiling(y2), input.Height); 24 | for (int x = 0; x < newWidth; ++x) 25 | { 26 | double x1 = x * descaleX; 27 | double x2 = x1 + descaleX; 28 | int x1i = (int)x1; 29 | int x2i = Math.Min((int)Math.Ceiling(x2), input.Width); 30 | double sum = 0; 31 | for (int oy = y1i; oy < y2i; ++oy) 32 | { 33 | var ry = Math.Min(oy + 1, y2) - Math.Max(oy, y1); 34 | for (int ox = x1i; ox < x2i; ++ox) 35 | { 36 | var rx = Math.Min(ox + 1, x2) - Math.Max(ox, x1); 37 | sum += rx * ry * input[ox, oy]; 38 | } 39 | } 40 | output[x, y] = sum * (scaleX * scaleY); 41 | } 42 | } 43 | return output; 44 | } 45 | public static DoubleMatrix Resize(DoubleMatrix input, double dpi) 46 | { 47 | return Resize(input, Doubles.RoundToInt(500.0 / dpi * input.Width), Doubles.RoundToInt(500.0 / dpi * input.Height)); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /SourceAFIS.Tests/FingerprintTemplateTest.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System; 3 | using NUnit.Framework; 4 | using SourceAFIS.Engine.Features; 5 | using SourceAFIS.Engine.Primitives; 6 | using SourceAFIS.Engine.Templates; 7 | 8 | // TODO: Port randomScaleMatch() from Java. 9 | namespace SourceAFIS 10 | { 11 | public class FingerprintTemplateTest 12 | { 13 | public static FingerprintTemplate Probe() => new FingerprintTemplate(FingerprintImageTest.Probe()); 14 | public static FingerprintTemplate Matching() => new FingerprintTemplate(FingerprintImageTest.Matching()); 15 | public static FingerprintTemplate Nonmatching() => new FingerprintTemplate(FingerprintImageTest.Nonmatching()); 16 | public static FingerprintTemplate ProbeGray() => new FingerprintTemplate(FingerprintImageTest.ProbeGray()); 17 | public static FingerprintTemplate MatchingGray() => new FingerprintTemplate(FingerprintImageTest.MatchingGray()); 18 | public static FingerprintTemplate NonmatchingGray() => new FingerprintTemplate(FingerprintImageTest.NonmatchingGray()); 19 | 20 | [Test] 21 | public void Constructor() => Probe(); 22 | [Test] 23 | public void RoundTripSerialization() 24 | { 25 | var mt = new FeatureTemplate(new(800, 600), new()); 26 | mt.Minutiae.Add(new(new(100, 200), FloatAngle.Pi, MinutiaType.Bifurcation)); 27 | mt.Minutiae.Add(new(new(300, 400), FloatAngle.HalfPi, MinutiaType.Ending)); 28 | var pt = new PersistentTemplate(mt); 29 | var t = new FingerprintTemplate(SerializationUtils.Serialize(pt)); 30 | t = new FingerprintTemplate(t.ToByteArray()); 31 | Assert.AreEqual(2, t.Minutiae.Length); 32 | var a = t.Minutiae[0]; 33 | var b = t.Minutiae[1]; 34 | Assert.AreEqual(new ShortPoint(100, 200), a.Position); 35 | Assert.AreEqual(Math.PI, a.Direction, 0.0000001); 36 | Assert.AreEqual(MinutiaType.Bifurcation, a.Type); 37 | Assert.AreEqual(new ShortPoint(300, 400), b.Position); 38 | Assert.AreEqual(0.5 * Math.PI, b.Direction, 0.0000001); 39 | Assert.AreEqual(MinutiaType.Ending, b.Type); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Extractor/LocalHistograms.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using SourceAFIS.Engine.Configuration; 3 | using SourceAFIS.Engine.Primitives; 4 | 5 | namespace SourceAFIS.Engine.Extractor 6 | { 7 | static class LocalHistograms 8 | { 9 | public static HistogramCube Create(BlockMap blocks, DoubleMatrix image) 10 | { 11 | var histogram = new HistogramCube(blocks.Primary.Blocks, Parameters.HistogramDepth); 12 | foreach (var block in blocks.Primary.Blocks.Iterate()) 13 | { 14 | var area = blocks.Primary.Block(block); 15 | for (int y = area.Top; y < area.Bottom; ++y) 16 | { 17 | for (int x = area.Left; x < area.Right; ++x) 18 | { 19 | int depth = (int)(image[x, y] * histogram.Bins); 20 | histogram.Increment(block, histogram.Constrain(depth)); 21 | } 22 | } 23 | } 24 | // https://sourceafis.machinezoo.com/transparency/histogram 25 | FingerprintTransparency.Current.Log("histogram", histogram); 26 | return histogram; 27 | } 28 | public static HistogramCube Smooth(BlockMap blocks, HistogramCube input) 29 | { 30 | var blocksAround = new IntPoint[] { new IntPoint(0, 0), new IntPoint(-1, 0), new IntPoint(0, -1), new IntPoint(-1, -1) }; 31 | var output = new HistogramCube(blocks.Secondary.Blocks, input.Bins); 32 | foreach (var corner in blocks.Secondary.Blocks.Iterate()) 33 | { 34 | foreach (var relative in blocksAround) 35 | { 36 | var block = corner + relative; 37 | if (blocks.Primary.Blocks.Contains(block)) 38 | { 39 | for (int i = 0; i < input.Bins; ++i) 40 | output.Add(corner, i, input[block, i]); 41 | } 42 | } 43 | } 44 | // https://sourceafis.machinezoo.com/transparency/smoothed-histogram 45 | FingerprintTransparency.Current.Log("smoothed-histogram", output); 46 | return output; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Primitives/CircularList.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | 5 | namespace SourceAFIS.Engine.Primitives 6 | { 7 | class CircularList : IList 8 | { 9 | readonly CircularArray inner = new CircularArray(16); 10 | 11 | public int Count => inner.Size; 12 | public bool IsReadOnly => false; 13 | public T this[int index] 14 | { 15 | get => inner[index]; 16 | set => inner[index] = value; 17 | } 18 | 19 | public int IndexOf(T item) 20 | { 21 | for (int i = 0; i < Count; ++i) 22 | if (EqualityComparer.Default.Equals(this[i], item)) 23 | return i; 24 | return -1; 25 | } 26 | public void Insert(int index, T item) 27 | { 28 | inner.Insert(index, 1); 29 | inner[index] = item; 30 | } 31 | public void RemoveAt(int index) => inner.Remove(index, 1); 32 | public void Add(T item) 33 | { 34 | inner.Insert(inner.Size, 1); 35 | inner[inner.Size - 1] = item; 36 | } 37 | public void Clear() { inner.Remove(0, inner.Size); } 38 | public bool Contains(T item) 39 | { 40 | for (int i = 0; i < Count; ++i) 41 | if (EqualityComparer.Default.Equals(this[i], item)) 42 | return true; 43 | return false; 44 | } 45 | public void CopyTo(T[] array, int at) 46 | { 47 | for (int i = 0; i < Count; ++i) 48 | array[at + i] = inner[i]; 49 | } 50 | public bool Remove(T item) 51 | { 52 | int index = IndexOf(item); 53 | if (index >= 0) 54 | { 55 | RemoveAt(index); 56 | return true; 57 | } 58 | else 59 | return false; 60 | } 61 | 62 | IEnumerator IEnumerable.GetEnumerator() 63 | { 64 | for (int i = 0; i < Count; ++i) 65 | yield return this[i]; 66 | } 67 | IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)this).GetEnumerator(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Features/SkeletonRidge.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System.Collections.Generic; 3 | using SourceAFIS.Engine.Configuration; 4 | using SourceAFIS.Engine.Primitives; 5 | 6 | namespace SourceAFIS.Engine.Features 7 | { 8 | class SkeletonRidge 9 | { 10 | public readonly SkeletonRidge Reversed; 11 | public readonly IList Points; 12 | SkeletonMinutia start; 13 | SkeletonMinutia end; 14 | 15 | public SkeletonMinutia Start 16 | { 17 | get => start; 18 | set 19 | { 20 | if (start != value) 21 | { 22 | if (start != null) 23 | { 24 | SkeletonMinutia detachFrom = start; 25 | start = null; 26 | detachFrom.DetachStart(this); 27 | } 28 | start = value; 29 | if (start != null) 30 | start.AttachStart(this); 31 | Reversed.end = value; 32 | } 33 | } 34 | } 35 | public SkeletonMinutia End 36 | { 37 | get => end; 38 | set 39 | { 40 | if (end != value) 41 | { 42 | end = value; 43 | Reversed.Start = value; 44 | } 45 | } 46 | } 47 | 48 | public SkeletonRidge() 49 | { 50 | Points = new CircularList(); 51 | Reversed = new SkeletonRidge(this); 52 | } 53 | SkeletonRidge(SkeletonRidge reversed) 54 | { 55 | Reversed = reversed; 56 | Points = new ReversedList(reversed.Points); 57 | } 58 | 59 | public void Detach() 60 | { 61 | Start = null; 62 | End = null; 63 | } 64 | public float Direction() 65 | { 66 | int first = Parameters.RidgeDirectionSkip; 67 | int last = Parameters.RidgeDirectionSkip + Parameters.RidgeDirectionSample - 1; 68 | if (last >= Points.Count) 69 | { 70 | int shift = last - Points.Count + 1; 71 | last -= shift; 72 | first -= shift; 73 | } 74 | if (first < 0) 75 | first = 0; 76 | return (float)DoubleAngle.Atan(Points[first], Points[last]); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /SourceAFIS.Tests/FingerprintImageTest.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System; 3 | using NUnit.Framework; 4 | using SourceAFIS.Engine.Primitives; 5 | 6 | namespace SourceAFIS 7 | { 8 | public class FingerprintImageTest 9 | { 10 | [Test] 11 | public void DecodePng() => new FingerprintImage(TestResources.Png()); 12 | 13 | void AssertSimilar(DoubleMatrix matrix, DoubleMatrix reference) 14 | { 15 | Assert.AreEqual(reference.Width, matrix.Width); 16 | Assert.AreEqual(reference.Height, matrix.Height); 17 | double delta = 0, max = -1, min = 1; 18 | for (int x = 0; x < matrix.Width; ++x) 19 | { 20 | for (int y = 0; y < matrix.Height; ++y) 21 | { 22 | delta += Math.Abs(matrix[x, y] - reference[x, y]); 23 | max = Math.Max(max, matrix[x, y]); 24 | min = Math.Min(min, matrix[x, y]); 25 | } 26 | } 27 | Assert.IsTrue(max > 0.75); 28 | Assert.IsTrue(min < 0.1); 29 | Assert.IsTrue(delta / (matrix.Width * matrix.Height) < 0.01); 30 | } 31 | void AssertSimilar(byte[] image, byte[] reference) => AssertSimilar(new FingerprintImage(image).Matrix, new FingerprintImage(reference).Matrix); 32 | 33 | [Test] 34 | public void DecodeJpeg() => AssertSimilar(TestResources.Jpeg(), TestResources.Png()); 35 | [Test] 36 | public void DecodeBmp() => AssertSimilar(TestResources.Bmp(), TestResources.Png()); 37 | 38 | public static FingerprintImage Probe() => new FingerprintImage(TestResources.Probe()); 39 | public static FingerprintImage Matching() => new FingerprintImage(TestResources.Matching()); 40 | public static FingerprintImage Nonmatching() => new FingerprintImage(TestResources.Nonmatching()); 41 | public static FingerprintImage ProbeGray() => new FingerprintImage(332, 533, TestResources.ProbeGray()); 42 | public static FingerprintImage MatchingGray() => new FingerprintImage(320, 407, TestResources.MatchingGray()); 43 | public static FingerprintImage NonmatchingGray() => new FingerprintImage(333, 435, TestResources.NonmatchingGray()); 44 | 45 | [Test] 46 | public void DecodeGray() 47 | { 48 | double score = new FingerprintMatcher(new FingerprintTemplate(ProbeGray())) 49 | .Match(new FingerprintTemplate(MatchingGray())); 50 | Assert.That(score, Is.GreaterThan(40)); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /SourceAFIS.Tests/Engine/Primitives/DoubleMatrixTest.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using NUnit.Framework; 3 | 4 | namespace SourceAFIS.Engine.Primitives 5 | { 6 | public class DoubleMatrixTest 7 | { 8 | DoubleMatrix m; 9 | 10 | [SetUp] 11 | public void SetUp() 12 | { 13 | m = new DoubleMatrix(3, 4); 14 | for (int x = 0; x < m.Width; ++x) 15 | for (int y = 0; y < m.Height; ++y) 16 | m[x, y] = 10 * x + y; 17 | } 18 | [Test] 19 | public void Constructor() 20 | { 21 | Assert.AreEqual(3, m.Width); 22 | Assert.AreEqual(4, m.Height); 23 | } 24 | [Test] 25 | public void ConstructorFromPoint() 26 | { 27 | var m = new DoubleMatrix(new IntPoint(3, 4)); 28 | Assert.AreEqual(3, m.Width); 29 | Assert.AreEqual(4, m.Height); 30 | } 31 | [Test] 32 | public void Size() 33 | { 34 | Assert.AreEqual(3, m.Size.X); 35 | Assert.AreEqual(4, m.Size.Y); 36 | } 37 | [Test] 38 | public void Get() 39 | { 40 | Assert.AreEqual(12, m[1, 2], 0.001); 41 | Assert.AreEqual(21, m[2, 1], 0.001); 42 | } 43 | [Test] 44 | public void GetAt() 45 | { 46 | Assert.AreEqual(3, m[new IntPoint(0, 3)], 0.001); 47 | Assert.AreEqual(22, m[new IntPoint(2, 2)], 0.001); 48 | } 49 | [Test] 50 | public void Set() 51 | { 52 | m[1, 2] = 101; 53 | Assert.AreEqual(101, m[1, 2], 0.001); 54 | } 55 | [Test] 56 | public void SetAt() 57 | { 58 | m[new IntPoint(2, 3)] = 101; 59 | Assert.AreEqual(101, m[2, 3], 0.001); 60 | } 61 | [Test] 62 | public void Add() 63 | { 64 | m.Add(2, 1, 100); 65 | Assert.AreEqual(121, m[2, 1], 0.001); 66 | } 67 | [Test] 68 | public void AddAt() 69 | { 70 | m.Add(new IntPoint(2, 3), 100); 71 | Assert.AreEqual(123, m[2, 3], 0.001); 72 | } 73 | [Test] 74 | public void Multiply() 75 | { 76 | m.Multiply(1, 3, 10); 77 | Assert.AreEqual(130, m[1, 3], 0.001); 78 | } 79 | [Test] 80 | public void MultiplyAt() 81 | { 82 | m.Multiply(new IntPoint(1, 2), 10); 83 | Assert.AreEqual(120, m[1, 2], 0.001); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Primitives/DoubleAngle.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System; 3 | 4 | namespace SourceAFIS.Engine.Primitives 5 | { 6 | static class DoubleAngle 7 | { 8 | public const double Pi2 = 2 * Math.PI; 9 | public const double InvPi2 = 1 / Pi2; 10 | public const double HalfPi = 0.5 * Math.PI; 11 | 12 | public static DoublePoint ToVector(double angle) => new DoublePoint(Math.Cos(angle), Math.Sin(angle)); 13 | public static double Atan(double x, double y) 14 | { 15 | double angle = Math.Atan2(y, x); 16 | return angle >= 0 ? angle : angle + Pi2; 17 | } 18 | public static double Atan(DoublePoint point) => Atan(point.X, point.Y); 19 | public static double Atan(IntPoint point) => Atan(point.X, point.Y); 20 | public static double Atan(IntPoint center, IntPoint point) => Atan(point - center); 21 | public static double ToOrientation(double angle) => angle < Math.PI ? 2 * angle : 2 * (angle - Math.PI); 22 | public static double FromOrientation(double angle) => 0.5 * angle; 23 | public static double Add(double start, double delta) 24 | { 25 | double angle = start + delta; 26 | return angle < Pi2 ? angle : angle - Pi2; 27 | } 28 | public static double BucketCenter(int bucket, int resolution) => Pi2 * (2 * bucket + 1) / (2 * resolution); 29 | public static int Quantize(double angle, int resolution) 30 | { 31 | int result = (int)(angle * InvPi2 * resolution); 32 | if (result < 0) 33 | return 0; 34 | else if (result >= resolution) 35 | return resolution - 1; 36 | else 37 | return result; 38 | } 39 | public static double Opposite(double angle) => angle < Math.PI ? angle + Math.PI : angle - Math.PI; 40 | public static double Distance(double first, double second) 41 | { 42 | double delta = Math.Abs(first - second); 43 | return delta <= Math.PI ? delta : Pi2 - delta; 44 | } 45 | public static double Difference(double first, double second) 46 | { 47 | double angle = first - second; 48 | return angle >= 0 ? angle : angle + Pi2; 49 | } 50 | public static double Complementary(double angle) 51 | { 52 | double complement = Pi2 - angle; 53 | return complement < Pi2 ? complement : complement - Pi2; 54 | } 55 | public static bool Normalized(double angle) => angle >= 0 && angle < Pi2; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /SourceAFIS.Tests/FingerprintTransparencyTest.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System.Collections.Generic; 3 | using NUnit.Framework; 4 | 5 | namespace SourceAFIS 6 | { 7 | public class FingerprintTransparencyTest 8 | { 9 | class TransparencyChecker : FingerprintTransparency 10 | { 11 | public readonly List Keys = new List(); 12 | 13 | public override void Take(string key, string mime, byte[] data) 14 | { 15 | Keys.Add(key); 16 | Assert.Contains(mime, new[] { "application/cbor", "text/plain" }); 17 | Assert.Greater(data.Length, 0); 18 | } 19 | } 20 | 21 | class TransparencyFilter : TransparencyChecker 22 | { 23 | public override bool Accepts(string key) => false; 24 | } 25 | 26 | [Test] 27 | public void Versioned() 28 | { 29 | using (var transparency = new TransparencyChecker()) 30 | { 31 | FingerprintTemplateTest.ProbeGray(); 32 | Assert.Contains("version", transparency.Keys); 33 | } 34 | } 35 | [Test] 36 | public void Extractor() 37 | { 38 | using (var transparency = new TransparencyChecker()) 39 | { 40 | FingerprintTemplateTest.ProbeGray(); 41 | Assert.IsNotEmpty(transparency.Keys); 42 | } 43 | } 44 | [Test] 45 | public void Matcher() 46 | { 47 | var probe = FingerprintTemplateTest.ProbeGray(); 48 | var matching = FingerprintTemplateTest.MatchingGray(); 49 | using (var transparency = new TransparencyChecker()) 50 | { 51 | new FingerprintMatcher(probe).Match(matching); 52 | Assert.IsNotEmpty(transparency.Keys); 53 | } 54 | } 55 | [Test] 56 | public void Deserialization() 57 | { 58 | var serialized = FingerprintTemplateTest.ProbeGray().ToByteArray(); 59 | using (var transparency = new TransparencyChecker()) 60 | { 61 | new FingerprintTemplate(serialized); 62 | Assert.IsNotEmpty(transparency.Keys); 63 | } 64 | } 65 | [Test] 66 | public void Filtered() 67 | { 68 | using (var transparency = new TransparencyFilter()) 69 | { 70 | new FingerprintMatcher(FingerprintTemplateTest.ProbeGray()) 71 | .Match(FingerprintTemplateTest.MatchingGray()); 72 | Assert.IsEmpty(transparency.Keys); 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Matcher/PairingGraph.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System.Collections.Generic; 3 | 4 | namespace SourceAFIS.Engine.Matcher 5 | { 6 | class PairingGraph 7 | { 8 | public readonly 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 readonly List SupportEdges = new List(); 14 | public bool SupportEnabled; 15 | public PairingGraph(MinutiaPairPool pool) => Pool = pool; 16 | public void ReserveProbe(FingerprintMatcher matcher) 17 | { 18 | int capacity = matcher.Template.Minutiae.Length; 19 | if (capacity > Tree.Length) 20 | { 21 | Tree = new MinutiaPair[capacity]; 22 | ByProbe = new MinutiaPair[capacity]; 23 | } 24 | } 25 | public void ReserveCandidate(FingerprintTemplate candidate) 26 | { 27 | int capacity = candidate.Minutiae.Length; 28 | if (ByCandidate.Length < capacity) 29 | ByCandidate = new MinutiaPair[capacity]; 30 | } 31 | public void AddPair(MinutiaPair pair) 32 | { 33 | Tree[Count] = pair; 34 | ByProbe[pair.Probe] = pair; 35 | ByCandidate[pair.Candidate] = pair; 36 | ++Count; 37 | } 38 | public void Support(MinutiaPair pair) 39 | { 40 | if (ByProbe[pair.Probe] != null && ByProbe[pair.Probe].Candidate == pair.Candidate) 41 | { 42 | ++ByProbe[pair.Probe].SupportingEdges; 43 | ++ByProbe[pair.ProbeRef].SupportingEdges; 44 | if (SupportEnabled) 45 | SupportEdges.Add(pair); 46 | else 47 | Pool.Release(pair); 48 | } 49 | else 50 | Pool.Release(pair); 51 | } 52 | public void Clear() 53 | { 54 | for (int i = 0; i < Count; ++i) 55 | { 56 | ByProbe[Tree[i].Probe] = null; 57 | ByCandidate[Tree[i].Candidate] = null; 58 | // Don't release root, just reset its supporting edge count. 59 | if (i > 0) 60 | Pool.Release(Tree[i]); 61 | else 62 | Tree[0].SupportingEdges = 0; 63 | Tree[i] = null; 64 | } 65 | Count = 0; 66 | if (SupportEnabled) 67 | { 68 | foreach (var pair in SupportEdges) 69 | Pool.Release(pair); 70 | SupportEdges.Clear(); 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Primitives/IntRect.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace SourceAFIS.Engine.Primitives 6 | { 7 | readonly struct IntRect : IEquatable 8 | { 9 | public readonly int X; 10 | public readonly int Y; 11 | public readonly int Width; 12 | public readonly int Height; 13 | 14 | public int Left => X; 15 | public int Top => Y; 16 | public int Right => X + Width; 17 | public int Bottom => Y + Height; 18 | public int Area => Width * Height; 19 | public IntPoint Center => new IntPoint((Left + Right) / 2, (Top + Bottom) / 2); 20 | 21 | public IntRect(int x, int y, int width, int height) 22 | { 23 | X = x; 24 | Y = y; 25 | Width = width; 26 | Height = height; 27 | } 28 | public IntRect(IntPoint size) 29 | { 30 | X = 0; 31 | Y = 0; 32 | Width = size.X; 33 | Height = size.Y; 34 | } 35 | 36 | public static bool operator ==(IntRect left, IntRect right) => left.Equals(right); 37 | public static bool operator !=(IntRect left, IntRect right) => !left.Equals(right); 38 | 39 | public override int GetHashCode() => ((X * 31 + Y) * 31 + Width) * 31 + Height; 40 | public bool Equals(IntRect other) => X == other.X && Y == other.Y && Width == other.Width && Height == other.Height; 41 | public override bool Equals(object other) => other is IntRect r && Equals(r); 42 | public override string ToString() => $"{Width}x{Height} @ [{X},{Y}]"; 43 | public static IntRect Between(int startX, int startY, int endX, int endY) => new IntRect(startX, startY, endX - startX, endY - startY); 44 | public static IntRect Between(IntPoint start, IntPoint end) => new IntRect(start.X, start.Y, end.X - start.X, end.Y - start.Y); 45 | public static IntRect Around(int x, int y, int radius) => Between(x - radius, y - radius, x + radius + 1, y + radius + 1); 46 | public static IntRect Around(IntPoint center, int radius) => Around(center.X, center.Y, radius); 47 | public IntRect Intersect(IntRect other) 48 | { 49 | return Between( 50 | new IntPoint(Math.Max(Left, other.Left), Math.Max(Top, other.Top)), 51 | new IntPoint(Math.Min(Right, other.Right), Math.Min(Bottom, other.Bottom))); 52 | } 53 | public IntRect Move(IntPoint delta) => new IntRect(X + delta.X, Y + delta.Y, Width, Height); 54 | 55 | public IEnumerable Iterate() 56 | { 57 | for (int y = Top; y < Bottom; ++y) 58 | for (int x = Left; x < Right; ++x) 59 | yield return new IntPoint(x, y); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Extractor/BlockOrientations.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using SourceAFIS.Engine.Configuration; 3 | using SourceAFIS.Engine.Primitives; 4 | 5 | namespace SourceAFIS.Engine.Extractor 6 | { 7 | static class BlockOrientations 8 | { 9 | static DoublePointMatrix Aggregate(DoublePointMatrix orientation, BlockMap blocks, BooleanMatrix mask) 10 | { 11 | var sums = new DoublePointMatrix(blocks.Primary.Blocks); 12 | foreach (var block in blocks.Primary.Blocks.Iterate()) 13 | { 14 | if (mask[block]) 15 | { 16 | var area = blocks.Primary.Block(block); 17 | for (int y = area.Top; y < area.Bottom; ++y) 18 | for (int x = area.Left; x < area.Right; ++x) 19 | sums.Add(block, orientation[x, y]); 20 | } 21 | } 22 | // https://sourceafis.machinezoo.com/transparency/block-orientation 23 | FingerprintTransparency.Current.Log("block-orientation", sums); 24 | return sums; 25 | } 26 | static DoublePointMatrix Smooth(DoublePointMatrix orientation, BooleanMatrix mask) 27 | { 28 | var size = mask.Size; 29 | var smoothed = new DoublePointMatrix(size); 30 | foreach (var block in size.Iterate()) 31 | if (mask[block]) 32 | { 33 | var neighbors = IntRect.Around(block, Parameters.OrientationSmoothingRadius).Intersect(new IntRect(size)); 34 | for (int ny = neighbors.Top; ny < neighbors.Bottom; ++ny) 35 | for (int nx = neighbors.Left; nx < neighbors.Right; ++nx) 36 | if (mask[nx, ny]) 37 | smoothed.Add(block, orientation[nx, ny]); 38 | } 39 | // https://sourceafis.machinezoo.com/transparency/smoothed-orientation 40 | FingerprintTransparency.Current.Log("smoothed-orientation", smoothed); 41 | return smoothed; 42 | } 43 | static DoubleMatrix Angles(DoublePointMatrix vectors, BooleanMatrix mask) 44 | { 45 | var size = mask.Size; 46 | var angles = new DoubleMatrix(size); 47 | foreach (var block in size.Iterate()) 48 | if (mask[block]) 49 | angles[block] = DoubleAngle.Atan(vectors[block]); 50 | return angles; 51 | } 52 | public static DoubleMatrix Compute(DoubleMatrix image, BooleanMatrix mask, BlockMap blocks) 53 | { 54 | var accumulated = PixelwiseOrientations.Compute(image, mask, blocks); 55 | var byBlock = Aggregate(accumulated, blocks, mask); 56 | var smooth = Smooth(byBlock, mask); 57 | return Angles(smooth, mask); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /SourceAFIS.Tests/Engine/Primitives/DoublePointMatrixTest.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using NUnit.Framework; 3 | 4 | namespace SourceAFIS.Engine.Primitives 5 | { 6 | public class DoublePointMatrixTest 7 | { 8 | DoublePointMatrix m; 9 | 10 | [SetUp] 11 | public void SetUp() 12 | { 13 | m = new DoublePointMatrix(4, 5); 14 | for (int x = 0; x < m.Width; ++x) 15 | for (int y = 0; y < m.Height; ++y) 16 | m[x, y] = new DoublePoint(10 * x, 10 * y); 17 | } 18 | [Test] 19 | public void Constructor() 20 | { 21 | Assert.AreEqual(4, m.Width); 22 | Assert.AreEqual(5, m.Height); 23 | } 24 | [Test] 25 | public void ConstructorFromPoint() 26 | { 27 | var m = new DoublePointMatrix(new IntPoint(4, 5)); 28 | Assert.AreEqual(4, m.Width); 29 | Assert.AreEqual(5, m.Height); 30 | } 31 | [Test] 32 | public void Get() 33 | { 34 | DoublePointTest.AssertPointEquals(new DoublePoint(20, 30), m[2, 3], 0.001); 35 | DoublePointTest.AssertPointEquals(new DoublePoint(30, 10), m[3, 1], 0.001); 36 | } 37 | [Test] 38 | public void GetAt() 39 | { 40 | DoublePointTest.AssertPointEquals(new DoublePoint(10, 20), m[new IntPoint(1, 2)], 0.001); 41 | DoublePointTest.AssertPointEquals(new DoublePoint(20, 40), m[new IntPoint(2, 4)], 0.001); 42 | } 43 | [Test] 44 | public void SetValues() 45 | { 46 | m.Set(2, 4, 101, 102); 47 | DoublePointTest.AssertPointEquals(new DoublePoint(101, 102), m[2, 4], 0.001); 48 | } 49 | [Test] 50 | public void Set() 51 | { 52 | m[1, 2] = new DoublePoint(101, 102); 53 | DoublePointTest.AssertPointEquals(new DoublePoint(101, 102), m[1, 2], 0.001); 54 | } 55 | [Test] 56 | public void SetAt() 57 | { 58 | m[new IntPoint(3, 2)] = new DoublePoint(101, 102); 59 | DoublePointTest.AssertPointEquals(new DoublePoint(101, 102), m[3, 2], 0.001); 60 | } 61 | [Test] 62 | public void AddValues() 63 | { 64 | m.Add(3, 1, 100, 200); 65 | DoublePointTest.AssertPointEquals(new DoublePoint(130, 210), m[3, 1], 0.001); 66 | } 67 | [Test] 68 | public void Add() 69 | { 70 | m.Add(2, 3, new DoublePoint(100, 200)); 71 | DoublePointTest.AssertPointEquals(new DoublePoint(120, 230), m[2, 3], 0.001); 72 | } 73 | [Test] 74 | public void AddAt() 75 | { 76 | m.Add(new IntPoint(2, 4), new DoublePoint(100, 200)); 77 | DoublePointTest.AssertPointEquals(new DoublePoint(120, 240), m[2, 4], 0.001); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /SourceAFIS.Tests/Engine/Primitives/HistogramCubeTest.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using NUnit.Framework; 3 | 4 | namespace SourceAFIS.Engine.Primitives 5 | { 6 | public class HistogramCubeTest 7 | { 8 | HistogramCube h; 9 | 10 | [SetUp] 11 | public void SetUp() 12 | { 13 | h = new HistogramCube(4, 5, 6); 14 | for (int x = 0; x < h.Width; ++x) 15 | for (int y = 0; y < h.Height; ++y) 16 | for (int z = 0; z < h.Bins; ++z) 17 | h[x, y, z] = 100 * x + 10 * y + z; 18 | } 19 | [Test] 20 | public void Constructor() 21 | { 22 | Assert.AreEqual(4, h.Width); 23 | Assert.AreEqual(5, h.Height); 24 | Assert.AreEqual(6, h.Bins); 25 | } 26 | [Test] 27 | public void Constrain() 28 | { 29 | Assert.AreEqual(3, h.Constrain(3)); 30 | Assert.AreEqual(0, h.Constrain(0)); 31 | Assert.AreEqual(5, h.Constrain(5)); 32 | Assert.AreEqual(0, h.Constrain(-1)); 33 | Assert.AreEqual(5, h.Constrain(6)); 34 | } 35 | [Test] 36 | public void Get() 37 | { 38 | Assert.AreEqual(234, h[2, 3, 4]); 39 | Assert.AreEqual(312, h[3, 1, 2]); 40 | } 41 | [Test] 42 | public void GetAt() 43 | { 44 | Assert.AreEqual(125, h[new IntPoint(1, 2), 5]); 45 | Assert.AreEqual(243, h[new IntPoint(2, 4), 3]); 46 | } 47 | [Test] 48 | public void Sum() 49 | { 50 | Assert.AreEqual(6 * 120 + 1 + 2 + 3 + 4 + 5, h.Sum(1, 2)); 51 | } 52 | [Test] 53 | public void SumAt() 54 | { 55 | Assert.AreEqual(6 * 340 + 1 + 2 + 3 + 4 + 5, h.Sum(new IntPoint(3, 4))); 56 | } 57 | [Test] 58 | public void Set() 59 | { 60 | h[2, 4, 3] = 1000; 61 | Assert.AreEqual(1000, h[2, 4, 3]); 62 | } 63 | [Test] 64 | public void SetAt() 65 | { 66 | h[new IntPoint(3, 1), 5] = 1000; 67 | Assert.AreEqual(1000, h[3, 1, 5]); 68 | } 69 | [Test] 70 | public void Add() 71 | { 72 | h.Add(1, 2, 4, 1000); 73 | Assert.AreEqual(1124, h[1, 2, 4]); 74 | } 75 | [Test] 76 | public void AddAt() 77 | { 78 | h.Add(new IntPoint(2, 4), 1, 1000); 79 | Assert.AreEqual(1241, h[2, 4, 1]); 80 | } 81 | [Test] 82 | public void Increment() 83 | { 84 | h.Increment(3, 4, 1); 85 | Assert.AreEqual(342, h[3, 4, 1]); 86 | } 87 | [Test] 88 | public void IncrementAt() 89 | { 90 | h.Increment(new IntPoint(2, 3), 5); 91 | Assert.AreEqual(236, h[2, 3, 5]); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /SourceAFIS.Tests/Engine/Primitives/DoublesTest.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using NUnit.Framework; 3 | 4 | namespace SourceAFIS.Engine.Primitives 5 | { 6 | public class DoublesTest 7 | { 8 | [Test] 9 | public void RoundToInt() 10 | { 11 | Assert.AreEqual(7, Doubles.RoundToInt(7)); 12 | Assert.AreEqual(5, Doubles.RoundToInt(5.4)); 13 | Assert.AreEqual(9, Doubles.RoundToInt(8.6)); 14 | Assert.AreEqual(4, Doubles.RoundToInt(3.5)); 15 | Assert.AreEqual(5, Doubles.RoundToInt(4.5)); 16 | Assert.AreEqual(-6, Doubles.RoundToInt(-6.4)); 17 | Assert.AreEqual(-8, Doubles.RoundToInt(-7.6)); 18 | Assert.AreEqual(-3, Doubles.RoundToInt(-3.5)); 19 | Assert.AreEqual(-4, Doubles.RoundToInt(-4.5)); 20 | } 21 | [Test] 22 | public void Sq() 23 | { 24 | Assert.AreEqual(6.25, Doubles.Sq(2.5), 0.001); 25 | Assert.AreEqual(6.25, Doubles.Sq(-2.5), 0.001); 26 | } 27 | [Test] 28 | public void Interpolate1D() 29 | { 30 | Assert.AreEqual(5, Doubles.Interpolate(3, 7, 0.5), 0.001); 31 | Assert.AreEqual(3, Doubles.Interpolate(3, 7, 0), 0.001); 32 | Assert.AreEqual(7, Doubles.Interpolate(3, 7, 1), 0.001); 33 | Assert.AreEqual(6, Doubles.Interpolate(7, 3, 0.25), 0.001); 34 | Assert.AreEqual(11, Doubles.Interpolate(7, 3, -1), 0.001); 35 | Assert.AreEqual(9, Doubles.Interpolate(3, 7, 1.5), 0.001); 36 | } 37 | [Test] 38 | public void Interpolate2D() 39 | { 40 | Assert.AreEqual(2, Doubles.Interpolate(3, 7, 2, 4, 0, 0), 0.001); 41 | Assert.AreEqual(4, Doubles.Interpolate(3, 7, 2, 4, 1, 0), 0.001); 42 | Assert.AreEqual(3, Doubles.Interpolate(3, 7, 2, 4, 0, 1), 0.001); 43 | Assert.AreEqual(7, Doubles.Interpolate(3, 7, 2, 4, 1, 1), 0.001); 44 | Assert.AreEqual(2.5, Doubles.Interpolate(3, 7, 2, 4, 0, 0.5), 0.001); 45 | Assert.AreEqual(5.5, Doubles.Interpolate(3, 7, 2, 4, 1, 0.5), 0.001); 46 | Assert.AreEqual(3, Doubles.Interpolate(3, 7, 2, 4, 0.5, 0), 0.001); 47 | Assert.AreEqual(5, Doubles.Interpolate(3, 7, 2, 4, 0.5, 1), 0.001); 48 | Assert.AreEqual(4, Doubles.Interpolate(3, 7, 2, 4, 0.5, 0.5), 0.001); 49 | } 50 | [Test] 51 | public void InterpolateExponential() 52 | { 53 | Assert.AreEqual(3, Doubles.InterpolateExponential(3, 10, 0), 0.001); 54 | Assert.AreEqual(10, Doubles.InterpolateExponential(3, 10, 1), 0.001); 55 | Assert.AreEqual(3, Doubles.InterpolateExponential(1, 9, 0.5), 0.001); 56 | Assert.AreEqual(27, Doubles.InterpolateExponential(1, 9, 1.5), 0.001); 57 | Assert.AreEqual(1 / 3.0, Doubles.InterpolateExponential(1, 9, -0.5), 0.001); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Matcher/RootEnumerator.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System.Collections.Generic; 3 | using SourceAFIS.Engine.Configuration; 4 | using SourceAFIS.Engine.Features; 5 | 6 | namespace SourceAFIS.Engine.Matcher 7 | { 8 | static class RootEnumerator 9 | { 10 | public static void Enumerate(FingerprintMatcher probe, FingerprintTemplate candidate, RootList roots) 11 | { 12 | var cminutiae = candidate.Minutiae; 13 | int lookups = 0; 14 | int tried = 0; 15 | foreach (bool shortEdges in new bool[] { false, true }) 16 | { 17 | for (int period = 1; period < cminutiae.Length; ++period) 18 | { 19 | for (int phase = 0; phase <= period; ++phase) 20 | { 21 | for (int creference = phase; creference < cminutiae.Length; creference += period + 1) 22 | { 23 | int cneighbor = (creference + period) % cminutiae.Length; 24 | var cedge = new EdgeShape(cminutiae[creference], cminutiae[cneighbor]); 25 | if (cedge.Length >= Parameters.MinRootEdgeLength ^ shortEdges) 26 | { 27 | List matches; 28 | if (probe.Hash.TryGetValue(EdgeHashes.Hash(cedge), out matches)) 29 | { 30 | foreach (var match in matches) 31 | { 32 | if (EdgeHashes.Matching(match.Shape, cedge)) 33 | { 34 | int duplicateKey = match.Reference << 16 | creference; 35 | if (roots.Duplicates.Add(duplicateKey)) 36 | { 37 | var pair = roots.Pool.Allocate(); 38 | pair.Probe = match.Reference; 39 | pair.Candidate = creference; 40 | roots.Add(pair); 41 | } 42 | ++tried; 43 | if (tried >= Parameters.MaxTriedRoots) 44 | return; 45 | } 46 | } 47 | } 48 | ++lookups; 49 | if (lookups >= Parameters.MaxRootEdgeLookups) 50 | return; 51 | } 52 | } 53 | } 54 | } 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /SourceAFIS.Tests/Engine/Primitives/CircularListTest.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System; 3 | using System.Collections.Generic; 4 | using NUnit.Framework; 5 | 6 | namespace SourceAFIS.Engine.Primitives 7 | { 8 | public class CircularListTest 9 | { 10 | CircularList l; 11 | 12 | [SetUp] 13 | public void SetUp() 14 | { 15 | l = new CircularList(); 16 | for (int i = 0; i < 5; ++i) 17 | l.Add(i + 1); 18 | } 19 | [Test] 20 | public void Add() 21 | { 22 | l.Add(100); 23 | Assert.AreEqual(new[] { 1, 2, 3, 4, 5, 100 }, l); 24 | } 25 | [Test] 26 | public void Insert() 27 | { 28 | l.Insert(3, 100); 29 | l.Insert(6, 200); 30 | l.Insert(0, 300); 31 | Assert.AreEqual(new[] { 300, 1, 2, 3, 100, 4, 5, 200 }, l); 32 | } 33 | [Test] 34 | public void InsertBounds() 35 | { 36 | Assert.Throws(() => l.Insert(6, 10)); 37 | } 38 | [Test] 39 | public void Clear() 40 | { 41 | l.Clear(); 42 | Assert.AreEqual(0, l.Count); 43 | } 44 | [Test] 45 | public void Contains() 46 | { 47 | Assert.IsTrue(l.Contains(3)); 48 | Assert.IsFalse(l.Contains(10)); 49 | } 50 | [Test] 51 | public void Get() 52 | { 53 | Assert.AreEqual(2, l[1]); 54 | Assert.AreEqual(4, l[3]); 55 | int discarded; 56 | Assert.Throws(() => discarded = l[5]); 57 | } 58 | [Test] 59 | public void IndexOf() 60 | { 61 | l.Add(3); 62 | Assert.AreEqual(2, l.IndexOf(3)); 63 | Assert.AreEqual(-1, l.IndexOf(10)); 64 | } 65 | [Test] 66 | public void Enumerator() 67 | { 68 | var c = new List(); 69 | foreach (var n in l) 70 | c.Add(n); 71 | Assert.AreEqual(new[] { 1, 2, 3, 4, 5 }, c); 72 | } 73 | [Test] 74 | public void RemoveAt() 75 | { 76 | l.RemoveAt(2); 77 | Assert.AreEqual(new[] { 1, 2, 4, 5 }, l); 78 | Assert.Throws(() => l.RemoveAt(5)); 79 | } 80 | [Test] 81 | public void Remove() 82 | { 83 | Assert.IsTrue(l.Remove(2)); 84 | Assert.IsFalse(l.Remove(10)); 85 | Assert.AreEqual(new[] { 1, 3, 4, 5 }, l); 86 | } 87 | [Test] 88 | public void Set() 89 | { 90 | l[2] = 10; 91 | Assert.AreEqual(new[] { 1, 2, 10, 4, 5 }, l); 92 | Assert.Throws(() => l[5] = 10); 93 | } 94 | [Test] 95 | public void Count() => Assert.AreEqual(5, l.Count); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Features/NeighborEdge.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Runtime.InteropServices; 5 | using SourceAFIS.Engine.Configuration; 6 | 7 | namespace SourceAFIS.Engine.Features 8 | { 9 | // Explicitly request sequential layout for predictable memory usage. 10 | [StructLayout(LayoutKind.Sequential)] 11 | readonly struct NeighborEdge 12 | { 13 | // Mind the field order. Let the floats in shape get aligned in the whole 16-byte structure. 14 | public readonly EdgeShape Shape; 15 | public readonly byte Neighbor; 16 | 17 | // Edge will have 2-byte alignment inherited from shape. 18 | public const int Memory = EdgeShape.Memory + 2; 19 | 20 | public NeighborEdge(Minutia[] minutiae, int reference, int neighbor) 21 | { 22 | Shape = new(minutiae[reference], minutiae[neighbor]); 23 | Neighbor = (byte)neighbor; 24 | } 25 | 26 | public static NeighborEdge[][] BuildTable(Minutia[] minutiae) 27 | { 28 | var edges = new NeighborEdge[minutiae.Length][]; 29 | var star = new List(); 30 | var allSqDistances = new int[minutiae.Length]; 31 | for (int reference = 0; reference < edges.Length; ++reference) 32 | { 33 | var referencePosition = minutiae[reference].Position; 34 | int maxSqDistance = int.MaxValue; 35 | if (minutiae.Length - 1 > Parameters.EdgeTableNeighbors) 36 | { 37 | for (int neighbor = 0; neighbor < minutiae.Length; ++neighbor) 38 | allSqDistances[neighbor] = (referencePosition - minutiae[neighbor].Position).LengthSq; 39 | Array.Sort(allSqDistances); 40 | maxSqDistance = allSqDistances[Parameters.EdgeTableNeighbors]; 41 | } 42 | for (int neighbor = 0; neighbor < minutiae.Length; ++neighbor) 43 | { 44 | if (neighbor != reference && (referencePosition - minutiae[neighbor].Position).LengthSq <= maxSqDistance) 45 | star.Add(new NeighborEdge(minutiae, reference, neighbor)); 46 | } 47 | star.Sort((a, b) => 48 | { 49 | int lengthCmp = a.Shape.Length.CompareTo(b.Shape.Length); 50 | if (lengthCmp != 0) 51 | return lengthCmp; 52 | return a.Neighbor.CompareTo(b.Neighbor); 53 | }); 54 | while (star.Count > Parameters.EdgeTableNeighbors) 55 | star.RemoveAt(star.Count - 1); 56 | edges[reference] = star.ToArray(); 57 | star.Clear(); 58 | } 59 | // https://sourceafis.machinezoo.com/transparency/edge-table 60 | FingerprintTransparency.Current.Log("edge-table", edges); 61 | return edges; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Primitives/PriorityQueue.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace SourceAFIS.Engine.Primitives 6 | { 7 | class PriorityQueue 8 | where T : class 9 | { 10 | readonly Comparer comparer; 11 | T[] heap; 12 | int size; 13 | 14 | public int Count => size; 15 | 16 | public PriorityQueue(Comparer comparer) 17 | { 18 | this.comparer = comparer; 19 | heap = new T[1]; 20 | } 21 | public PriorityQueue() : this(Comparer.Default) { } 22 | 23 | public void Clear() 24 | { 25 | for (int i = 0; i < size; ++i) 26 | heap[i] = null; 27 | size = 0; 28 | } 29 | void Enlarge() 30 | { 31 | T[] larger = new T[2 * heap.Length]; 32 | Array.Copy(heap, larger, heap.Length); 33 | heap = larger; 34 | } 35 | static int Left(int parent) => 2 * parent + 1; 36 | static int Right(int parent) => 2 * parent + 2; 37 | static int Parent(int child) => child - 1 >> 1; 38 | void BubbleUp(int bottom) 39 | { 40 | for (int child = bottom; child > 0; child = Parent(child)) 41 | { 42 | int parent = Parent(child); 43 | if (comparer.Compare(heap[parent], heap[child]) < 0) 44 | break; 45 | T tmp = heap[child]; 46 | heap[child] = heap[parent]; 47 | heap[parent] = tmp; 48 | } 49 | } 50 | public void Add(T item) 51 | { 52 | if (size >= heap.Length) 53 | Enlarge(); 54 | heap[size] = item; 55 | BubbleUp(size); 56 | ++size; 57 | } 58 | void BubbleDown() 59 | { 60 | int parent = 0; 61 | while (true) 62 | { 63 | int left = Left(parent); 64 | int right = Right(parent); 65 | if (left >= size) 66 | break; 67 | int child; 68 | if (right >= size || comparer.Compare(heap[left], heap[right]) < 0) 69 | child = left; 70 | else 71 | child = right; 72 | if (comparer.Compare(heap[parent], heap[child]) < 0) 73 | break; 74 | T tmp = heap[parent]; 75 | heap[parent] = heap[child]; 76 | heap[child] = tmp; 77 | parent = child; 78 | } 79 | } 80 | public T Peek() 81 | { 82 | if (size <= 0) 83 | throw new InvalidOperationException(); 84 | return heap[0]; 85 | } 86 | public T Remove() 87 | { 88 | if (size <= 0) 89 | throw new InvalidOperationException(); 90 | T result = heap[0]; 91 | heap[0] = heap[size - 1]; 92 | --size; 93 | BubbleDown(); 94 | return result; 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Features/EdgeShape.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System; 3 | using System.Runtime.InteropServices; 4 | using SourceAFIS.Engine.Primitives; 5 | 6 | namespace SourceAFIS.Engine.Features 7 | { 8 | // No padding, so that edge structs can put fields where padding would otherwise be. 9 | [StructLayout(LayoutKind.Sequential, Pack = 2)] 10 | readonly struct EdgeShape 11 | { 12 | const int PolarCacheBits = 8; 13 | const int PolarCacheRadius = 1 << PolarCacheBits; 14 | 15 | static readonly short[] PolarDistanceCache = new short[Integers.Sq(PolarCacheRadius)]; 16 | static readonly float[] PolarAngleCache = new float[Integers.Sq(PolarCacheRadius)]; 17 | 18 | // Mind the field order. Floats first to ensure they are aligned despite 2-byte struct packing. 19 | public readonly float ReferenceAngle; 20 | public readonly float NeighborAngle; 21 | public readonly short Length; 22 | 23 | // This will only be the case with sequential layout and 2-byte packing. 24 | public const int Memory = 2 * sizeof(float) + sizeof(short); 25 | 26 | static EdgeShape() 27 | { 28 | for (int y = 0; y < PolarCacheRadius; ++y) 29 | { 30 | for (int x = 0; x < PolarCacheRadius; ++x) 31 | { 32 | PolarDistanceCache[y * PolarCacheRadius + x] = (short)Doubles.RoundToInt(Math.Sqrt(Doubles.Sq(x) + Doubles.Sq(y))); 33 | if (y > 0 || x > 0) 34 | PolarAngleCache[y * PolarCacheRadius + x] = (float)DoubleAngle.Atan(x, y); 35 | else 36 | PolarAngleCache[y * PolarCacheRadius + x] = 0; 37 | } 38 | } 39 | } 40 | 41 | public EdgeShape(int length, float referenceAngle, float neighborAngle) 42 | { 43 | Length = (short)length; 44 | ReferenceAngle = referenceAngle; 45 | NeighborAngle = neighborAngle; 46 | } 47 | public EdgeShape(Minutia reference, Minutia neighbor) 48 | { 49 | var vector = neighbor.Position - reference.Position; 50 | float quadrant = 0; 51 | int x = vector.X; 52 | int y = vector.Y; 53 | if (y < 0) 54 | { 55 | x = -x; 56 | y = -y; 57 | quadrant = FloatAngle.Pi; 58 | } 59 | if (x < 0) 60 | { 61 | int tmp = -x; 62 | x = y; 63 | y = tmp; 64 | quadrant += FloatAngle.HalfPi; 65 | } 66 | int shift = 32 - (int)Integers.LeadingZeros(((uint)x | (uint)y) >> PolarCacheBits); 67 | int offset = (y >> shift) * PolarCacheRadius + (x >> shift); 68 | Length = (short)(PolarDistanceCache[offset] << shift); 69 | float angle = PolarAngleCache[offset] + quadrant; 70 | ReferenceAngle = FloatAngle.Difference(reference.Direction, angle); 71 | NeighborAngle = FloatAngle.Difference(neighbor.Direction, FloatAngle.Opposite(angle)); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Matcher/MatcherEngine.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System; 3 | 4 | namespace SourceAFIS.Engine.Matcher 5 | { 6 | static class MatcherEngine 7 | { 8 | public static double Match(FingerprintMatcher probe, FingerprintTemplate candidate) 9 | { 10 | // Thread-local storage is fairly fast, but it's still a hash lookup, 11 | // so do not access FingerprintTransparency.Current repeatedly in tight loops. 12 | var transparency = FingerprintTransparency.Current; 13 | var thread = MatcherThread.Current; 14 | try 15 | { 16 | thread.Pairing.ReserveProbe(probe); 17 | thread.Pairing.ReserveCandidate(candidate); 18 | thread.Pairing.SupportEnabled = transparency.AcceptsPairing(); 19 | RootEnumerator.Enumerate(probe, candidate, thread.Roots); 20 | // https://sourceafis.machinezoo.com/transparency/root-pairs 21 | transparency.LogRootPairs(thread.Roots.Count, thread.Roots.Pairs); 22 | double high = 0; 23 | int best = -1; 24 | for (int i = 0; i < thread.Roots.Count; ++i) 25 | { 26 | EdgeSpider.Crawl(probe.Template.Edges, candidate.Edges, thread.Pairing, thread.Roots.Pairs[i], thread.Queue); 27 | // https://sourceafis.machinezoo.com/transparency/pairing 28 | transparency.LogPairing(thread.Pairing); 29 | Scoring.Compute(probe.Template, candidate, thread.Pairing, thread.Score); 30 | // https://sourceafis.machinezoo.com/transparency/score 31 | transparency.LogScore(thread.Score); 32 | double partial = thread.Score.ShapedScore; 33 | if (best < 0 || partial > high) 34 | { 35 | high = partial; 36 | best = i; 37 | } 38 | thread.Pairing.Clear(); 39 | } 40 | if (best >= 0 && (transparency.AcceptsBestPairing() || transparency.AcceptsBestScore())) 41 | { 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 | } 56 | catch (Exception) 57 | { 58 | MatcherThread.Kill(); 59 | throw; 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Extractor/VoteFilter.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System; 3 | using SourceAFIS.Engine.Primitives; 4 | 5 | namespace SourceAFIS.Engine.Extractor 6 | { 7 | static class VoteFilter 8 | { 9 | public static BooleanMatrix Vote(BooleanMatrix input, BooleanMatrix mask, int radius, double majority, int borderDistance) 10 | { 11 | var size = input.Size; 12 | var rect = new IntRect(borderDistance, borderDistance, size.X - 2 * borderDistance, size.Y - 2 * borderDistance); 13 | int[] thresholds = new int[Integers.Sq(2 * radius + 1) + 1]; 14 | for (int i = 0; i < thresholds.Length; ++i) 15 | thresholds[i] = (int)Math.Ceiling(majority * i); 16 | var counts = new IntMatrix(size); 17 | var output = new BooleanMatrix(size); 18 | for (int y = rect.Top; y < rect.Bottom; ++y) 19 | { 20 | int superTop = y - radius - 1; 21 | int superBottom = y + radius; 22 | int yMin = Math.Max(0, y - radius); 23 | int yMax = Math.Min(size.Y - 1, y + radius); 24 | int yRange = yMax - yMin + 1; 25 | for (int x = rect.Left; x < rect.Right; ++x) 26 | if (mask == null || mask[x, y]) 27 | { 28 | int left = x > 0 ? counts[x - 1, y] : 0; 29 | int top = y > 0 ? counts[x, y - 1] : 0; 30 | int diagonal = x > 0 && y > 0 ? counts[x - 1, y - 1] : 0; 31 | int xMin = Math.Max(0, x - radius); 32 | int xMax = Math.Min(size.X - 1, x + radius); 33 | int ones; 34 | if (left > 0 && top > 0 && diagonal > 0) 35 | { 36 | ones = top + left - diagonal - 1; 37 | int superLeft = x - radius - 1; 38 | int superRight = x + radius; 39 | if (superLeft >= 0 && superTop >= 0 && input[superLeft, superTop]) 40 | ++ones; 41 | if (superLeft >= 0 && superBottom < size.Y && input[superLeft, superBottom]) 42 | --ones; 43 | if (superRight < size.X && superTop >= 0 && input[superRight, superTop]) 44 | --ones; 45 | if (superRight < size.X && superBottom < size.Y && input[superRight, superBottom]) 46 | ++ones; 47 | } 48 | else 49 | { 50 | ones = 0; 51 | for (int ny = yMin; ny <= yMax; ++ny) 52 | for (int nx = xMin; nx <= xMax; ++nx) 53 | if (input[nx, ny]) 54 | ++ones; 55 | } 56 | counts[x, y] = ones + 1; 57 | if (ones >= thresholds[yRange * (xMax - xMin + 1)]) 58 | output[x, y] = true; 59 | } 60 | } 61 | return output; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Extractor/FeatureExtractor.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using SourceAFIS.Engine.Configuration; 3 | using SourceAFIS.Engine.Extractor.Minutiae; 4 | using SourceAFIS.Engine.Extractor.Skeletons; 5 | using SourceAFIS.Engine.Features; 6 | using SourceAFIS.Engine.Primitives; 7 | using SourceAFIS.Engine.Templates; 8 | 9 | namespace SourceAFIS.Engine.Extractor 10 | { 11 | static class FeatureExtractor 12 | { 13 | public static FeatureTemplate Extract(DoubleMatrix raw, double dpi) 14 | { 15 | // https://sourceafis.machinezoo.com/transparency/decoded-image 16 | FingerprintTransparency.Current.Log("decoded-image", raw); 17 | raw = ImageResizer.Resize(raw, dpi); 18 | // https://sourceafis.machinezoo.com/transparency/scaled-image 19 | FingerprintTransparency.Current.Log("scaled-image", raw); 20 | var blocks = new BlockMap(raw.Width, raw.Height, Parameters.BlockSize); 21 | // https://sourceafis.machinezoo.com/transparency/blocks 22 | FingerprintTransparency.Current.Log("blocks", blocks); 23 | var histogram = LocalHistograms.Create(blocks, raw); 24 | var smoothHistogram = LocalHistograms.Smooth(blocks, histogram); 25 | var mask = SegmentationMask.Compute(blocks, histogram); 26 | var equalized = ImageEqualization.Equalize(blocks, raw, smoothHistogram, mask); 27 | var orientation = BlockOrientations.Compute(equalized, mask, blocks); 28 | var smoothed = OrientedSmoothing.Parallel(equalized, orientation, mask, blocks); 29 | var orthogonal = OrientedSmoothing.Orthogonal(smoothed, orientation, mask, blocks); 30 | var binary = BinarizedImage.Binarize(smoothed, orthogonal, mask, blocks); 31 | var pixelMask = SegmentationMask.Pixelwise(mask, blocks); 32 | BinarizedImage.Cleanup(binary, pixelMask); 33 | // https://sourceafis.machinezoo.com/transparency/pixel-mask 34 | FingerprintTransparency.Current.Log("pixel-mask", pixelMask); 35 | var inverted = BinarizedImage.Invert(binary, pixelMask); 36 | var innerMask = SegmentationMask.Inner(pixelMask); 37 | var ridges = SkeletonGraphs.Create(binary, SkeletonType.Ridges); 38 | var valleys = SkeletonGraphs.Create(inverted, SkeletonType.Valleys); 39 | var template = new FeatureTemplate(raw.Size.ToShort(), MinutiaCollector.Collect(ridges, valleys)); 40 | // https://sourceafis.machinezoo.com/transparency/skeleton-minutiae 41 | FingerprintTransparency.Current.Log("skeleton-minutiae", template); 42 | InnerMinutiaeFilter.Apply(template.Minutiae, innerMask); 43 | // https://sourceafis.machinezoo.com/transparency/inner-minutiae 44 | FingerprintTransparency.Current.Log("inner-minutiae", template); 45 | MinutiaCloudFilter.Apply(template.Minutiae); 46 | // https://sourceafis.machinezoo.com/transparency/removed-minutia-clouds 47 | FingerprintTransparency.Current.Log("removed-minutia-clouds", template); 48 | template = new(template.Size, TopMinutiaeFilter.Apply(template.Minutiae)); 49 | // https://sourceafis.machinezoo.com/transparency/top-minutiae 50 | FingerprintTransparency.Current.Log("top-minutiae", template); 51 | return template; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Templates/PersistentTemplate.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System; 3 | using System.Collections.Generic; 4 | using SourceAFIS.Engine.Features; 5 | using SourceAFIS.Engine.Primitives; 6 | 7 | namespace SourceAFIS.Engine.Templates 8 | { 9 | class PersistentTemplate 10 | { 11 | public string Version; 12 | public short Width; 13 | public short Height; 14 | public short[] PositionsX; 15 | public short[] PositionsY; 16 | public float[] Directions; 17 | public string Types; 18 | 19 | public PersistentTemplate() { } 20 | public PersistentTemplate(FeatureTemplate template) 21 | { 22 | Version = FingerprintCompatibility.Version + "-net"; 23 | Width = template.Size.X; 24 | Height = template.Size.Y; 25 | int count = template.Minutiae.Count; 26 | PositionsX = new short[count]; 27 | PositionsY = new short[count]; 28 | Directions = new float[count]; 29 | var chars = new char[count]; 30 | for (int i = 0; i < count; ++i) 31 | { 32 | var minutia = template.Minutiae[i]; 33 | PositionsX[i] = minutia.Position.X; 34 | PositionsY[i] = minutia.Position.Y; 35 | Directions[i] = minutia.Direction; 36 | chars[i] = minutia.Type == MinutiaType.Bifurcation ? 'B' : 'E'; 37 | } 38 | Types = new string(chars); 39 | } 40 | 41 | public FeatureTemplate Decode() 42 | { 43 | var minutiae = new List(); 44 | for (int i = 0; i < Types.Length; ++i) 45 | { 46 | var type = Types[i] == 'B' ? MinutiaType.Bifurcation : MinutiaType.Ending; 47 | minutiae.Add(new(new(PositionsX[i], PositionsY[i]), Directions[i], type)); 48 | } 49 | return new FeatureTemplate(new(Width, Height), minutiae); 50 | } 51 | public void Validate() 52 | { 53 | // Width and height are informative only. Don't validate them. Ditto for version string. 54 | if (PositionsX == null) 55 | throw new NullReferenceException("Null array of X positions."); 56 | if (PositionsY == null) 57 | throw new NullReferenceException("Null array of Y positions."); 58 | if (Directions == null) 59 | throw new NullReferenceException("Null array of minutia directions."); 60 | if (Types == null) 61 | throw new NullReferenceException("Null minutia type string."); 62 | if (PositionsX.Length != Types.Length || PositionsY.Length != Types.Length || Directions.Length != Types.Length) 63 | throw new ArgumentException("Inconsistent lengths of minutia property arrays."); 64 | for (int i = 0; i < Types.Length; ++i) 65 | { 66 | if (Math.Abs(PositionsX[i]) > 10_000 || Math.Abs(PositionsY[i]) > 10_000) 67 | throw new ArgumentException("Minutia position out of range."); 68 | if (!FloatAngle.Normalized(Directions[i])) 69 | throw new ArgumentException("Denormalized minutia direction."); 70 | if (Types[i] != 'E' && Types[i] != 'B') 71 | throw new ArgumentException("Unknown minutia type."); 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Extractor/SegmentationMask.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using SourceAFIS.Engine.Configuration; 3 | using SourceAFIS.Engine.Primitives; 4 | 5 | namespace SourceAFIS.Engine.Extractor 6 | { 7 | static class SegmentationMask 8 | { 9 | static BooleanMatrix Filter(BooleanMatrix input) 10 | { 11 | return VoteFilter.Vote(input, null, Parameters.BlockErrorsVoteRadius, Parameters.BlockErrorsVoteMajority, Parameters.BlockErrorsVoteBorderDistance); 12 | } 13 | public static BooleanMatrix Compute(BlockMap blocks, HistogramCube histogram) 14 | { 15 | var contrast = ClippedContrast.Compute(blocks, histogram); 16 | var mask = AbsoluteContrastMask.Compute(contrast); 17 | mask.Merge(RelativeContrastMask.Compute(contrast, blocks)); 18 | // https://sourceafis.machinezoo.com/transparency/combined-mask 19 | FingerprintTransparency.Current.Log("combined-mask", mask); 20 | mask.Merge(Filter(mask)); 21 | mask.Invert(); 22 | mask.Merge(Filter(mask)); 23 | mask.Merge(Filter(mask)); 24 | mask.Merge(VoteFilter.Vote(mask, null, Parameters.MaskVoteRadius, Parameters.MaskVoteMajority, Parameters.MaskVoteBorderDistance)); 25 | // https://sourceafis.machinezoo.com/transparency/filtered-mask 26 | FingerprintTransparency.Current.Log("filtered-mask", mask); 27 | return mask; 28 | } 29 | public static BooleanMatrix Pixelwise(BooleanMatrix mask, BlockMap blocks) 30 | { 31 | var pixelized = new BooleanMatrix(blocks.Pixels); 32 | foreach (var block in blocks.Primary.Blocks.Iterate()) 33 | if (mask[block]) 34 | foreach (var pixel in blocks.Primary.Block(block).Iterate()) 35 | pixelized[pixel] = true; 36 | return pixelized; 37 | } 38 | static BooleanMatrix Shrink(BooleanMatrix mask, int amount) 39 | { 40 | var size = mask.Size; 41 | var shrunk = new BooleanMatrix(size); 42 | for (int y = amount; y < size.Y - amount; ++y) 43 | for (int x = amount; x < size.X - amount; ++x) 44 | shrunk[x, y] = mask[x, y - amount] && mask[x, y + amount] && mask[x - amount, y] && mask[x + amount, y]; 45 | return shrunk; 46 | } 47 | public static BooleanMatrix Inner(BooleanMatrix outer) 48 | { 49 | var size = outer.Size; 50 | var inner = new BooleanMatrix(size); 51 | for (int y = 1; y < size.Y - 1; ++y) 52 | for (int x = 1; x < size.X - 1; ++x) 53 | inner[x, y] = outer[x, y]; 54 | if (Parameters.InnerMaskBorderDistance >= 1) 55 | inner = Shrink(inner, 1); 56 | int total = 1; 57 | for (int step = 1; total + step <= Parameters.InnerMaskBorderDistance; step *= 2) 58 | { 59 | inner = Shrink(inner, step); 60 | total += step; 61 | } 62 | if (total < Parameters.InnerMaskBorderDistance) 63 | inner = Shrink(inner, Parameters.InnerMaskBorderDistance - total); 64 | // https://sourceafis.machinezoo.com/transparency/inner-mask 65 | FingerprintTransparency.Current.Log("inner-mask", inner); 66 | return inner; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /SourceAFIS.Tests/Engine/Primitives/ReversedListTest.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System; 3 | using System.Collections.Generic; 4 | using NUnit.Framework; 5 | 6 | namespace SourceAFIS.Engine.Primitives 7 | { 8 | public class ReversedListTest 9 | { 10 | List o; 11 | ReversedList r; 12 | 13 | [SetUp] 14 | public void SetUp() 15 | { 16 | o = new List(); 17 | r = new ReversedList(o); 18 | for (int i = 0; i < 5; ++i) 19 | o.Add(i + 1); 20 | } 21 | [Test] 22 | public void Add() 23 | { 24 | r.Add(10); 25 | Assert.AreEqual(new[] { 10, 1, 2, 3, 4, 5 }, o); 26 | } 27 | [Test] 28 | public void Insert() 29 | { 30 | r.Insert(1, 10); 31 | r.Insert(6, 20); 32 | r.Insert(0, 30); 33 | Assert.AreEqual(new[] { 20, 1, 2, 3, 4, 10, 5, 30 }, o); 34 | } 35 | [Test] 36 | public void InsertBounds() => Assert.Throws(() => r.Insert(6, 10)); 37 | [Test] 38 | public void Clear() 39 | { 40 | r.Clear(); 41 | Assert.AreEqual(0, o.Count); 42 | } 43 | [Test] 44 | public void Contains() 45 | { 46 | Assert.IsTrue(r.Contains(3)); 47 | Assert.IsFalse(r.Contains(10)); 48 | } 49 | [Test] 50 | public void Get() 51 | { 52 | Assert.AreEqual(4, r[1]); 53 | Assert.AreEqual(2, r[3]); 54 | int discarded; 55 | Assert.Throws(() => discarded = r[-1]); 56 | Assert.Throws(() => discarded = r[5]); 57 | } 58 | [Test] 59 | public void IndexOf() 60 | { 61 | r.Add(4); 62 | Assert.AreEqual(1, r.IndexOf(4)); 63 | Assert.AreEqual(-1, r.IndexOf(10)); 64 | } 65 | [Test] 66 | public void Enumerable() 67 | { 68 | var c = new List(); 69 | foreach (var n in r) 70 | c.Add(n); 71 | Assert.AreEqual(new[] { 5, 4, 3, 2, 1 }, c); 72 | } 73 | [Test] 74 | public void RemoveAt() 75 | { 76 | r.RemoveAt(1); 77 | Assert.AreEqual(new[] { 5, 3, 2, 1 }, r); 78 | } 79 | [Test] 80 | public void RemoveAtBounds() 81 | { 82 | Assert.Throws(() => r.RemoveAt(-1)); 83 | Assert.Throws(() => r.RemoveAt(5)); 84 | } 85 | [Test] 86 | public void Remove() 87 | { 88 | r.Add(4); 89 | Assert.IsTrue(r.Remove(4)); 90 | Assert.IsFalse(r.Remove(10)); 91 | Assert.AreEqual(new[] { 5, 3, 2, 1, 4 }, r); 92 | } 93 | [Test] 94 | public void Set() 95 | { 96 | r[1] = 10; 97 | Assert.AreEqual(new[] { 5, 10, 3, 2, 1 }, r); 98 | Assert.Throws(() => r[-1] = 10); 99 | Assert.Throws(() => r[5] = 10); 100 | } 101 | [Test] 102 | public void Count() => Assert.AreEqual(5, r.Count); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Extractor/BinarizedImage.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using SourceAFIS.Engine.Configuration; 3 | using SourceAFIS.Engine.Primitives; 4 | 5 | namespace SourceAFIS.Engine.Extractor 6 | { 7 | static class BinarizedImage 8 | { 9 | public static BooleanMatrix Binarize(DoubleMatrix input, DoubleMatrix baseline, BooleanMatrix mask, BlockMap blocks) 10 | { 11 | var size = input.Size; 12 | var binarized = new BooleanMatrix(size); 13 | foreach (var block in blocks.Primary.Blocks.Iterate()) 14 | { 15 | if (mask[block]) 16 | { 17 | var rect = blocks.Primary.Block(block); 18 | for (int y = rect.Top; y < rect.Bottom; ++y) 19 | for (int x = rect.Left; x < rect.Right; ++x) 20 | if (input[x, y] - baseline[x, y] > 0) 21 | binarized[x, y] = true; 22 | } 23 | } 24 | // https://sourceafis.machinezoo.com/transparency/binarized-image 25 | FingerprintTransparency.Current.Log("binarized-image", binarized); 26 | return binarized; 27 | } 28 | static void RemoveCrosses(BooleanMatrix input) 29 | { 30 | var size = input.Size; 31 | bool any = true; 32 | while (any) 33 | { 34 | any = false; 35 | for (int y = 0; y < size.Y - 1; ++y) 36 | for (int x = 0; x < size.X - 1; ++x) 37 | if (input[x, y] && input[x + 1, y + 1] && !input[x, y + 1] && !input[x + 1, y] || input[x, y + 1] && input[x + 1, y] && !input[x, y] && !input[x + 1, y + 1]) 38 | { 39 | input[x, y] = false; 40 | input[x, y + 1] = false; 41 | input[x + 1, y] = false; 42 | input[x + 1, y + 1] = false; 43 | any = true; 44 | } 45 | } 46 | } 47 | public static void Cleanup(BooleanMatrix binary, BooleanMatrix mask) 48 | { 49 | var size = binary.Size; 50 | var inverted = new BooleanMatrix(binary); 51 | inverted.Invert(); 52 | var islands = VoteFilter.Vote(inverted, mask, Parameters.BinarizedVoteRadius, Parameters.BinarizedVoteMajority, Parameters.BinarizedVoteBorderDistance); 53 | var holes = VoteFilter.Vote(binary, mask, Parameters.BinarizedVoteRadius, Parameters.BinarizedVoteMajority, Parameters.BinarizedVoteBorderDistance); 54 | for (int y = 0; y < size.Y; ++y) 55 | for (int x = 0; x < size.X; ++x) 56 | binary[x, y] = binary[x, y] && !islands[x, y] || holes[x, y]; 57 | RemoveCrosses(binary); 58 | // https://sourceafis.machinezoo.com/transparency/filtered-binary-image 59 | FingerprintTransparency.Current.Log("filtered-binary-image", binary); 60 | } 61 | public static BooleanMatrix Invert(BooleanMatrix binary, BooleanMatrix mask) 62 | { 63 | var size = binary.Size; 64 | var inverted = new BooleanMatrix(size); 65 | for (int y = 0; y < size.Y; ++y) 66 | for (int x = 0; x < size.X; ++x) 67 | inverted[x, y] = !binary[x, y] && mask[x, y]; 68 | return inverted; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Primitives/SerializationUtils.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Runtime.CompilerServices; 7 | using Dahomey.Cbor; 8 | using Dahomey.Cbor.Serialization; 9 | using Dahomey.Cbor.Serialization.Conventions; 10 | using Dahomey.Cbor.Serialization.Converters.Mappings; 11 | using Dahomey.Cbor.Util; 12 | 13 | namespace SourceAFIS.Engine.Primitives 14 | { 15 | static class SerializationUtils 16 | { 17 | // Dahomey.Cbor requires default constructor even if we perform only serialization. 18 | // We will give it fake object factory to keep it happy. 19 | class NoDefaultConstructor : ICreatorMapping 20 | { 21 | public IReadOnlyCollection MemberNames => null; 22 | 23 | public void Initialize() { } 24 | public object CreateInstance(Dictionary values) => throw new NotImplementedException(); 25 | } 26 | 27 | // Conventions consistent with Java. 28 | class ConsistentConvention : IObjectMappingConvention 29 | { 30 | static void CollectFields(ObjectMapping mapping, Type type) 31 | { 32 | // Reflection will not give us private fields of base classes, so walk base classes explicitly. 33 | var parent = type.BaseType; 34 | if (parent != null) 35 | CollectFields(mapping, parent); 36 | foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly)) 37 | if (field.GetCustomAttribute() == null) 38 | mapping.MapMember(field, field.FieldType); 39 | } 40 | public void Apply(SerializationRegistry registry, ObjectMapping mapping) 41 | { 42 | // Java field naming convention. 43 | mapping.SetNamingConvention(new CamelCaseNamingConvention()); 44 | // Do not serialize properties, only fields. Include both public and private fields. 45 | CollectFields(mapping, mapping.ObjectType); 46 | // Allow serialization of objects without default constructor. We don't need deserialization. 47 | var constructors = mapping.ObjectType.GetConstructors(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); 48 | if (!constructors.Any(c => c.GetParameters().Length == 0)) 49 | mapping.SetCreatorMapping(new NoDefaultConstructor()); 50 | } 51 | } 52 | 53 | class ConsistentConventionProvider : IObjectMappingConventionProvider 54 | { 55 | public IObjectMappingConvention GetConvention(Type type) => new ConsistentConvention(); 56 | } 57 | 58 | static readonly CborOptions Options = new CborOptions(); 59 | 60 | static SerializationUtils() 61 | { 62 | Options.Registry.ObjectMappingConventionRegistry.RegisterProvider(new ConsistentConventionProvider()); 63 | } 64 | 65 | public static byte[] Serialize(object data) 66 | { 67 | using (var buffer = new ByteBufferWriter()) 68 | { 69 | Cbor.Serialize(data, data.GetType(), buffer, Options); 70 | return buffer.WrittenSpan.ToArray(); 71 | } 72 | } 73 | public static T Deserialize(byte[] bytes) => Cbor.Deserialize(bytes, Options); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /SourceAFIS.Tests/Engine/Primitives/IntRectTest.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System.Collections.Generic; 3 | using NUnit.Framework; 4 | 5 | namespace SourceAFIS.Engine.Primitives 6 | { 7 | public class IntRectTest 8 | { 9 | [Test] 10 | public void Constructor() 11 | { 12 | var r = new IntRect(2, 3, 10, 20); 13 | Assert.AreEqual(2, r.X); 14 | Assert.AreEqual(3, r.Y); 15 | Assert.AreEqual(10, r.Width); 16 | Assert.AreEqual(20, r.Height); 17 | } 18 | [Test] 19 | public void ConstructorFromPoint() 20 | { 21 | var r = new IntRect(new IntPoint(2, 3)); 22 | Assert.AreEqual(0, r.X); 23 | Assert.AreEqual(0, r.Y); 24 | Assert.AreEqual(2, r.Width); 25 | Assert.AreEqual(3, r.Height); 26 | } 27 | [Test] 28 | public void Left() => Assert.AreEqual(2, new IntRect(2, 3, 4, 5).Left); 29 | [Test] 30 | public void Right() => Assert.AreEqual(6, new IntRect(2, 3, 4, 5).Right); 31 | [Test] 32 | public void Bottom() => Assert.AreEqual(3, new IntRect(2, 3, 4, 5).Top); 33 | [Test] 34 | public void Top() => Assert.AreEqual(8, new IntRect(2, 3, 4, 5).Bottom); 35 | [Test] 36 | public void Area() => Assert.AreEqual(20, new IntRect(2, 3, 4, 5).Area); 37 | [Test] 38 | public void BetweenCoordinates() => Assert.AreEqual(new IntRect(2, 3, 4, 5), IntRect.Between(2, 3, 6, 8)); 39 | [Test] 40 | public void BetweenPoints() => Assert.AreEqual(new IntRect(2, 3, 4, 5), IntRect.Between(new IntPoint(2, 3), new IntPoint(6, 8))); 41 | [Test] 42 | public void AroundCoordinates() => Assert.AreEqual(new IntRect(2, 3, 5, 5), IntRect.Around(4, 5, 2)); 43 | [Test] 44 | public void AroundPoint() => Assert.AreEqual(new IntRect(2, 3, 5, 5), IntRect.Around(new IntPoint(4, 5), 2)); 45 | [Test] 46 | public void Center() 47 | { 48 | Assert.AreEqual(new IntPoint(4, 5), new IntRect(2, 3, 4, 4).Center); 49 | Assert.AreEqual(new IntPoint(4, 5), new IntRect(2, 3, 5, 5).Center); 50 | Assert.AreEqual(new IntPoint(2, 3), new IntRect(2, 3, 0, 0).Center); 51 | } 52 | [Test] 53 | public void Move() => Assert.AreEqual(new IntRect(12, 23, 4, 5), new IntRect(2, 3, 4, 5).Move(new IntPoint(10, 20))); 54 | [Test] 55 | public void Intersect() 56 | { 57 | Assert.AreEqual(new IntRect(58, 30, 2, 5), new IntRect(20, 30, 40, 50).Intersect(new IntRect(58, 27, 7, 8))); 58 | Assert.AreEqual(new IntRect(20, 77, 5, 3), new IntRect(20, 30, 40, 50).Intersect(new IntRect(18, 77, 7, 8))); 59 | Assert.AreEqual(new IntRect(30, 40, 20, 30), new IntRect(20, 30, 40, 50).Intersect(new IntRect(30, 40, 20, 30))); 60 | } 61 | [Test] 62 | public void Iterate() 63 | { 64 | var l = new List(); 65 | foreach (var p in new IntRect(4, 5, 2, 3).Iterate()) 66 | l.Add(p); 67 | Assert.AreEqual(new[] { new IntPoint(4, 5), new IntPoint(5, 5), new IntPoint(4, 6), new IntPoint(5, 6), new IntPoint(4, 7), new IntPoint(5, 7) }, l); 68 | foreach (var p in new IntRect(2, 3, 0, 3).Iterate()) 69 | Assert.Fail(); 70 | foreach (var p in new IntRect(2, 3, 3, 0).Iterate()) 71 | Assert.Fail(); 72 | foreach (var p in new IntRect(2, 3, -1, 3).Iterate()) 73 | Assert.Fail(); 74 | foreach (var p in new IntRect(2, 3, 3, -1).Iterate()) 75 | Assert.Fail(); 76 | } 77 | [Test] 78 | public void ToStringReadable() => Assert.AreEqual("10x20 @ [2,3]", new IntRect(2, 3, 10, 20).ToString()); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Matcher/EdgeSpider.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System.Collections.Generic; 3 | using SourceAFIS.Engine.Configuration; 4 | using SourceAFIS.Engine.Features; 5 | using SourceAFIS.Engine.Primitives; 6 | 7 | namespace SourceAFIS.Engine.Matcher 8 | { 9 | static class EdgeSpider 10 | { 11 | static readonly float ComplementaryMaxAngleError = FloatAngle.Complementary(Parameters.MaxAngleError); 12 | static List MatchPairs(NeighborEdge[] pstar, NeighborEdge[] cstar, MinutiaPairPool pool) 13 | { 14 | var results = new List(); 15 | int start = 0; 16 | int end = 0; 17 | for (int cindex = 0; cindex < cstar.Length; ++cindex) 18 | { 19 | var cedge = cstar[cindex]; 20 | while (start < pstar.Length && pstar[start].Shape.Length < cedge.Shape.Length - Parameters.MaxDistanceError) 21 | ++start; 22 | if (end < start) 23 | end = start; 24 | while (end < pstar.Length && pstar[end].Shape.Length <= cedge.Shape.Length + Parameters.MaxDistanceError) 25 | ++end; 26 | for (int pindex = start; pindex < end; ++pindex) 27 | { 28 | var pedge = pstar[pindex]; 29 | float rdiff = FloatAngle.Difference(pedge.Shape.ReferenceAngle, cedge.Shape.ReferenceAngle); 30 | if (rdiff <= Parameters.MaxAngleError || rdiff >= ComplementaryMaxAngleError) 31 | { 32 | float ndiff = FloatAngle.Difference(pedge.Shape.NeighborAngle, cedge.Shape.NeighborAngle); 33 | if (ndiff <= Parameters.MaxAngleError || ndiff >= ComplementaryMaxAngleError) 34 | { 35 | var pair = pool.Allocate(); 36 | pair.Probe = pedge.Neighbor; 37 | pair.Candidate = cedge.Neighbor; 38 | pair.Distance = cedge.Shape.Length; 39 | results.Add(pair); 40 | } 41 | } 42 | } 43 | } 44 | return results; 45 | } 46 | static void CollectEdges(NeighborEdge[][] pedges, NeighborEdge[][] cedges, PairingGraph pairing, PriorityQueue queue) 47 | { 48 | var reference = pairing.Tree[pairing.Count - 1]; 49 | var pstar = pedges[reference.Probe]; 50 | var cstar = cedges[reference.Candidate]; 51 | foreach (var pair in MatchPairs(pstar, cstar, pairing.Pool)) 52 | { 53 | pair.ProbeRef = reference.Probe; 54 | pair.CandidateRef = reference.Candidate; 55 | if (pairing.ByCandidate[pair.Candidate] == null && pairing.ByProbe[pair.Probe] == null) 56 | queue.Add(pair); 57 | else 58 | pairing.Support(pair); 59 | } 60 | } 61 | static void SkipPaired(PairingGraph pairing, PriorityQueue queue) 62 | { 63 | while (queue.Count > 0 && (pairing.ByProbe[queue.Peek().Probe] != null || pairing.ByCandidate[queue.Peek().Candidate] != null)) 64 | pairing.Support(queue.Remove()); 65 | } 66 | public static void Crawl(NeighborEdge[][] pedges, NeighborEdge[][] cedges, PairingGraph pairing, MinutiaPair root, PriorityQueue queue) 67 | { 68 | queue.Add(root); 69 | do 70 | { 71 | pairing.AddPair(queue.Remove()); 72 | CollectEdges(pedges, cedges, pairing, queue); 73 | SkipPaired(pairing, queue); 74 | } while (queue.Count > 0); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Primitives/CircularArray.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System; 3 | 4 | namespace SourceAFIS.Engine.Primitives 5 | { 6 | class CircularArray 7 | { 8 | internal T[] Array; 9 | internal int Head; 10 | public int Size; 11 | 12 | public T this[int index] 13 | { 14 | get 15 | { 16 | ValidateItemIndex(index); 17 | return Array[Location(index)]; 18 | } 19 | set 20 | { 21 | ValidateItemIndex(index); 22 | Array[Location(index)] = value; 23 | } 24 | } 25 | 26 | public CircularArray(int capacity) => Array = new T[capacity]; 27 | 28 | internal void ValidateItemIndex(int index) 29 | { 30 | if (index < 0 || index >= Size) 31 | throw new ArgumentOutOfRangeException(); 32 | } 33 | internal void ValidateCursorIndex(int index) 34 | { 35 | if (index < 0 || index > Size) 36 | throw new ArgumentOutOfRangeException(); 37 | } 38 | internal int Location(int index) => Head + index < Array.Length ? Head + index : Head + index - Array.Length; 39 | internal void Enlarge() 40 | { 41 | T[] enlarged = new T[2 * Array.Length]; 42 | for (int i = 0; i < Size; ++i) 43 | enlarged[i] = Array[Location(i)]; 44 | Array = enlarged; 45 | Head = 0; 46 | } 47 | internal void Move(int from, int to, int length) 48 | { 49 | if (from < to) 50 | { 51 | for (int i = length - 1; i >= 0; --i) 52 | this[to + i] = this[from + i]; 53 | } 54 | else if (from > to) 55 | { 56 | for (int i = 0; i < length; ++i) 57 | this[to + i] = this[from + i]; 58 | } 59 | } 60 | public void Insert(int index, int amount) 61 | { 62 | ValidateCursorIndex(index); 63 | if (amount < 0) 64 | throw new ArgumentOutOfRangeException(); 65 | while (Size + amount > Array.Length) 66 | Enlarge(); 67 | if (2 * index >= Size) 68 | { 69 | Size += amount; 70 | Move(index, index + amount, Size - amount - index); 71 | } 72 | else 73 | { 74 | Head -= amount; 75 | Size += amount; 76 | if (Head < 0) 77 | Head += Array.Length; 78 | Move(amount, 0, index); 79 | } 80 | for (int i = 0; i < amount; ++i) 81 | this[index + i] = default; 82 | } 83 | public void Remove(int index, int amount) 84 | { 85 | ValidateCursorIndex(index); 86 | if (amount < 0) 87 | throw new ArgumentOutOfRangeException(); 88 | ValidateCursorIndex(index + amount); 89 | if (2 * index >= Size - amount) 90 | { 91 | Move(index + amount, index, Size - amount - index); 92 | for (int i = 0; i < amount; ++i) 93 | this[Size - i - 1] = default; 94 | Size -= amount; 95 | } 96 | else 97 | { 98 | Move(0, amount, index); 99 | for (int i = 0; i < amount; ++i) 100 | this[i] = default; 101 | Head += amount; 102 | Size -= amount; 103 | if (Head >= Array.Length) 104 | Head -= Array.Length; 105 | } 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Extractor/OrientedSmoothing.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System; 3 | using System.Collections.Generic; 4 | using SourceAFIS.Engine.Configuration; 5 | using SourceAFIS.Engine.Primitives; 6 | 7 | namespace SourceAFIS.Engine.Extractor 8 | { 9 | static class OrientedSmoothing 10 | { 11 | static IntPoint[][] Lines(int resolution, int radius, double step) 12 | { 13 | var result = new IntPoint[resolution][]; 14 | for (int orientationIndex = 0; orientationIndex < resolution; ++orientationIndex) 15 | { 16 | var line = new List(); 17 | line.Add(IntPoint.Zero); 18 | var direction = DoubleAngle.ToVector(DoubleAngle.FromOrientation(DoubleAngle.BucketCenter(orientationIndex, resolution))); 19 | for (double r = radius; r >= 0.5; r /= step) 20 | { 21 | var sample = (r * direction).Round(); 22 | if (!line.Contains(sample)) 23 | { 24 | line.Add(sample); 25 | line.Add(-sample); 26 | } 27 | } 28 | result[orientationIndex] = line.ToArray(); 29 | } 30 | return result; 31 | } 32 | static DoubleMatrix Smooth(DoubleMatrix input, DoubleMatrix orientation, BooleanMatrix mask, BlockMap blocks, double angle, IntPoint[][] lines) 33 | { 34 | var output = new DoubleMatrix(input.Size); 35 | foreach (var block in blocks.Primary.Blocks.Iterate()) 36 | { 37 | if (mask[block]) 38 | { 39 | var line = lines[DoubleAngle.Quantize(DoubleAngle.Add(orientation[block], angle), lines.Length)]; 40 | foreach (var linePoint in line) 41 | { 42 | var target = blocks.Primary.Block(block); 43 | var source = target.Move(linePoint).Intersect(new IntRect(blocks.Pixels)); 44 | target = source.Move(-linePoint); 45 | for (int y = target.Top; y < target.Bottom; ++y) 46 | for (int x = target.Left; x < target.Right; ++x) 47 | output.Add(x, y, input[x + linePoint.X, y + linePoint.Y]); 48 | } 49 | var blockArea = blocks.Primary.Block(block); 50 | for (int y = blockArea.Top; y < blockArea.Bottom; ++y) 51 | for (int x = blockArea.Left; x < blockArea.Right; ++x) 52 | output.Multiply(x, y, 1.0 / line.Length); 53 | } 54 | } 55 | return output; 56 | } 57 | public static DoubleMatrix Parallel(DoubleMatrix input, DoubleMatrix orientation, BooleanMatrix mask, BlockMap blocks) 58 | { 59 | var lines = Lines(Parameters.ParallelSmoothingResolution, Parameters.ParallelSmoothingRadius, Parameters.ParallelSmoothingStep); 60 | var smoothed = Smooth(input, orientation, mask, blocks, 0, lines); 61 | // https://sourceafis.machinezoo.com/transparency/parallel-smoothing 62 | FingerprintTransparency.Current.Log("parallel-smoothing", smoothed); 63 | return smoothed; 64 | } 65 | public static DoubleMatrix Orthogonal(DoubleMatrix input, DoubleMatrix orientation, BooleanMatrix mask, BlockMap blocks) 66 | { 67 | var lines = Lines(Parameters.OrthogonalSmoothingResolution, Parameters.OrthogonalSmoothingRadius, Parameters.OrthogonalSmoothingStep); 68 | var smoothed = Smooth(input, orientation, mask, blocks, Math.PI, lines); 69 | // https://sourceafis.machinezoo.com/transparency/orthogonal-smoothing 70 | FingerprintTransparency.Current.Log("orthogonal-smoothing", smoothed); 71 | return smoothed; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Extractor/Skeletons/SkeletonGapFilter.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using SourceAFIS.Engine.Configuration; 3 | using SourceAFIS.Engine.Features; 4 | using SourceAFIS.Engine.Primitives; 5 | 6 | namespace SourceAFIS.Engine.Extractor.Skeletons 7 | { 8 | static class SkeletonGapFilter 9 | { 10 | static void AddGapRidge(BooleanMatrix shadow, SkeletonGap gap, IntPoint[] line) 11 | { 12 | var ridge = new SkeletonRidge(); 13 | foreach (var point in line) 14 | ridge.Points.Add(point); 15 | ridge.Start = gap.End1; 16 | ridge.End = gap.End2; 17 | foreach (var point in line) 18 | shadow[point] = true; 19 | } 20 | static bool IsRidgeOverlapping(IntPoint[] line, BooleanMatrix shadow) 21 | { 22 | for (int i = Parameters.ToleratedGapOverlap; i < line.Length - Parameters.ToleratedGapOverlap; ++i) 23 | if (shadow[line[i]]) 24 | return true; 25 | return false; 26 | } 27 | static IntPoint AngleSampleForGapRemoval(SkeletonMinutia minutia) 28 | { 29 | var ridge = minutia.Ridges[0]; 30 | if (Parameters.GapAngleOffset < ridge.Points.Count) 31 | return ridge.Points[Parameters.GapAngleOffset]; 32 | else 33 | return ridge.End.Position; 34 | } 35 | static bool IsWithinGapLimits(SkeletonMinutia end1, SkeletonMinutia end2) 36 | { 37 | int distanceSq = (end1.Position - end2.Position).LengthSq; 38 | if (distanceSq <= Integers.Sq(Parameters.MaxRuptureSize)) 39 | return true; 40 | if (distanceSq > Integers.Sq(Parameters.MaxGapSize)) 41 | return false; 42 | double gapDirection = DoubleAngle.Atan(end1.Position, end2.Position); 43 | double direction1 = DoubleAngle.Atan(end1.Position, AngleSampleForGapRemoval(end1)); 44 | if (DoubleAngle.Distance(direction1, DoubleAngle.Opposite(gapDirection)) > Parameters.MaxGapAngle) 45 | return false; 46 | double direction2 = DoubleAngle.Atan(end2.Position, AngleSampleForGapRemoval(end2)); 47 | if (DoubleAngle.Distance(direction2, gapDirection) > Parameters.MaxGapAngle) 48 | return false; 49 | return true; 50 | } 51 | public static void Apply(Skeleton skeleton) 52 | { 53 | var queue = new PriorityQueue(); 54 | foreach (var end1 in skeleton.Minutiae) 55 | if (end1.Ridges.Count == 1 && end1.Ridges[0].Points.Count >= Parameters.ShortestJoinedEnding) 56 | foreach (var end2 in skeleton.Minutiae) 57 | if (end2 != end1 && end2.Ridges.Count == 1 && end1.Ridges[0].End != end2 58 | && end2.Ridges[0].Points.Count >= Parameters.ShortestJoinedEnding && IsWithinGapLimits(end1, end2)) 59 | { 60 | var gap = new SkeletonGap(); 61 | gap.Distance = (end1.Position - end2.Position).LengthSq; 62 | gap.End1 = end1; 63 | gap.End2 = end2; 64 | queue.Add(gap); 65 | } 66 | var shadow = skeleton.Shadow(); 67 | while (queue.Count > 0) 68 | { 69 | var gap = queue.Remove(); 70 | if (gap.End1.Ridges.Count == 1 && gap.End2.Ridges.Count == 1) 71 | { 72 | var line = gap.End1.Position.LineTo(gap.End2.Position); 73 | if (!IsRidgeOverlapping(line, shadow)) 74 | AddGapRidge(shadow, gap, line); 75 | } 76 | } 77 | SkeletonKnotFilter.Apply(skeleton); 78 | // https://sourceafis.machinezoo.com/transparency/removed-gaps 79 | FingerprintTransparency.Current.LogSkeleton("removed-gaps", skeleton); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Configuration/Parameters.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System; 3 | using SourceAFIS.Engine.Primitives; 4 | 5 | namespace SourceAFIS.Engine.Configuration 6 | { 7 | static class Parameters 8 | { 9 | public const int BlockSize = 15; 10 | public const int HistogramDepth = 256; 11 | public const double ClippedContrast = 0.08; 12 | public const double MinAbsoluteContrast = 17 / 255.0; 13 | public const double MinRelativeContrast = 0.34; 14 | public const int RelativeContrastSample = 168568; 15 | public const double RelativeContrastPercentile = 0.49; 16 | public const int MaskVoteRadius = 7; 17 | public const double MaskVoteMajority = 0.51; 18 | public const int MaskVoteBorderDistance = 4; 19 | public const int BlockErrorsVoteRadius = 1; 20 | public const double BlockErrorsVoteMajority = 0.7; 21 | public const int BlockErrorsVoteBorderDistance = 4; 22 | public const double MaxEqualizationScaling = 3.99; 23 | public const double MinEqualizationScaling = 0.25; 24 | public const double MinOrientationRadius = 2; 25 | public const double MaxOrientationRadius = 6; 26 | public const int OrientationSplit = 50; 27 | public const int OrientationsChecked = 20; 28 | public const int OrientationSmoothingRadius = 1; 29 | public const int ParallelSmoothingResolution = 32; 30 | public const int ParallelSmoothingRadius = 7; 31 | public const double ParallelSmoothingStep = 1.59; 32 | public const int OrthogonalSmoothingResolution = 11; 33 | public const int OrthogonalSmoothingRadius = 4; 34 | public const double OrthogonalSmoothingStep = 1.11; 35 | public const int BinarizedVoteRadius = 2; 36 | public const double BinarizedVoteMajority = 0.61; 37 | public const int BinarizedVoteBorderDistance = 17; 38 | public const int InnerMaskBorderDistance = 14; 39 | public const double MaskDisplacement = 10.06; 40 | public const int MinutiaCloudRadius = 20; 41 | public const int MaxCloudSize = 4; 42 | public const int MaxMinutiae = 100; 43 | public const int SortByNeighbor = 5; 44 | public const int EdgeTableNeighbors = 9; 45 | public const int ThinningIterations = 26; 46 | public const int MaxPoreArm = 41; 47 | public const int ShortestJoinedEnding = 7; 48 | public const int MaxRuptureSize = 5; 49 | public const int MaxGapSize = 20; 50 | public const int GapAngleOffset = 22; 51 | public const int ToleratedGapOverlap = 2; 52 | public const int MinTailLength = 21; 53 | public const int MinFragmentLength = 22; 54 | public const int MaxDistanceError = 13; 55 | public const float MaxAngleError = FloatAngle.Pi / 180 * 10; 56 | public const double MaxGapAngle = Math.PI / 180 * 45; 57 | public const int RidgeDirectionSample = 21; 58 | public const int RidgeDirectionSkip = 1; 59 | public const int MaxTriedRoots = 70; 60 | public const int MinRootEdgeLength = 58; 61 | public const int MaxRootEdgeLookups = 1633; 62 | public const int MinSupportingEdges = 1; 63 | public const double DistanceErrorFlatness = 0.69; 64 | public const double AngleErrorFlatness = 0.27; 65 | public const double MinutiaScore = 0.032; 66 | public const double MinutiaFractionScore = 8.98; 67 | public const double MinutiaTypeScore = 0.629; 68 | public const double SupportedMinutiaScore = 0.193; 69 | public const double EdgeScore = 0.265; 70 | public const double DistanceAccuracyScore = 9.9; 71 | public const double AngleAccuracyScore = 2.79; 72 | public const double ThresholdFmrMax = 8.48; 73 | public const double ThresholdFmr2 = 11.12; 74 | public const double ThresholdFmr10 = 14.15; 75 | public const double ThresholdFmr100 = 18.22; 76 | public const double ThresholdFmr1000 = 22.39; 77 | public const double ThresholdFmr10K = 27.24; 78 | public const double ThresholdFmr100K = 32.01; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Extractor/ImageEqualization.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System; 3 | using System.Collections.Generic; 4 | using SourceAFIS.Engine.Configuration; 5 | using SourceAFIS.Engine.Primitives; 6 | 7 | namespace SourceAFIS.Engine.Extractor 8 | { 9 | static class ImageEqualization 10 | { 11 | public static DoubleMatrix Equalize(BlockMap blocks, DoubleMatrix image, HistogramCube histogram, BooleanMatrix blockMask) 12 | { 13 | const double rangeMin = -1; 14 | const double rangeMax = 1; 15 | const double rangeSize = rangeMax - rangeMin; 16 | double widthMax = rangeSize / histogram.Bins * Parameters.MaxEqualizationScaling; 17 | double widthMin = rangeSize / histogram.Bins * Parameters.MinEqualizationScaling; 18 | var limitedMin = new double[histogram.Bins]; 19 | var limitedMax = new double[histogram.Bins]; 20 | var dequantized = new double[histogram.Bins]; 21 | for (int i = 0; i < histogram.Bins; ++i) 22 | { 23 | limitedMin[i] = Math.Max(i * widthMin + rangeMin, rangeMax - (histogram.Bins - 1 - i) * widthMax); 24 | limitedMax[i] = Math.Min(i * widthMax + rangeMin, rangeMax - (histogram.Bins - 1 - i) * widthMin); 25 | dequantized[i] = i / (double)(histogram.Bins - 1); 26 | } 27 | var mappings = new Dictionary(); 28 | foreach (var corner in blocks.Secondary.Blocks.Iterate()) 29 | { 30 | double[] mapping = new double[histogram.Bins]; 31 | mappings[corner] = mapping; 32 | if (blockMask.Get(corner, false) 33 | || blockMask.Get(corner.X - 1, corner.Y, false) 34 | || blockMask.Get(corner.X, corner.Y - 1, false) 35 | || blockMask.Get(corner.X - 1, corner.Y - 1, false)) 36 | { 37 | double step = rangeSize / histogram.Sum(corner); 38 | double top = rangeMin; 39 | for (int i = 0; i < histogram.Bins; ++i) 40 | { 41 | double band = histogram[corner, i] * step; 42 | double equalized = top + dequantized[i] * band; 43 | top += band; 44 | if (equalized < limitedMin[i]) 45 | equalized = limitedMin[i]; 46 | if (equalized > limitedMax[i]) 47 | equalized = limitedMax[i]; 48 | mapping[i] = equalized; 49 | } 50 | } 51 | } 52 | var result = new DoubleMatrix(blocks.Pixels); 53 | foreach (var block in blocks.Primary.Blocks.Iterate()) 54 | { 55 | var area = blocks.Primary.Block(block); 56 | if (blockMask[block]) 57 | { 58 | var topleft = mappings[block]; 59 | var topright = mappings[new IntPoint(block.X + 1, block.Y)]; 60 | var bottomleft = mappings[new IntPoint(block.X, block.Y + 1)]; 61 | var bottomright = mappings[new IntPoint(block.X + 1, block.Y + 1)]; 62 | for (int y = area.Top; y < area.Bottom; ++y) 63 | for (int x = area.Left; x < area.Right; ++x) 64 | { 65 | int depth = histogram.Constrain((int)(image[x, y] * histogram.Bins)); 66 | double rx = (x - area.X + 0.5) / area.Width; 67 | double ry = (y - area.Y + 0.5) / area.Height; 68 | result[x, y] = Doubles.Interpolate(bottomleft[depth], bottomright[depth], topleft[depth], topright[depth], rx, ry); 69 | } 70 | } 71 | else 72 | { 73 | for (int y = area.Top; y < area.Bottom; ++y) 74 | for (int x = area.Left; x < area.Right; ++x) 75 | result[x, y] = -1; 76 | } 77 | } 78 | // https://sourceafis.machinezoo.com/transparency/equalized-image 79 | FingerprintTransparency.Current.Log("equalized-image", result); 80 | return result; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Matcher/EdgeHashes.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System; 3 | using System.Collections.Generic; 4 | using SourceAFIS.Engine.Configuration; 5 | using SourceAFIS.Engine.Features; 6 | using SourceAFIS.Engine.Primitives; 7 | 8 | namespace SourceAFIS.Engine.Matcher 9 | { 10 | static class EdgeHashes 11 | { 12 | static readonly float ComplementaryMaxAngleError = FloatAngle.Complementary(Parameters.MaxAngleError); 13 | public static int Hash(EdgeShape edge) 14 | { 15 | int lengthBin = edge.Length / Parameters.MaxDistanceError; 16 | int referenceAngleBin = (int)(edge.ReferenceAngle / Parameters.MaxAngleError); 17 | int neighborAngleBin = (int)(edge.NeighborAngle / Parameters.MaxAngleError); 18 | return (referenceAngleBin << 24) + (neighborAngleBin << 16) + lengthBin; 19 | } 20 | public static bool Matching(EdgeShape probe, EdgeShape candidate) 21 | { 22 | int lengthDelta = probe.Length - candidate.Length; 23 | if (lengthDelta >= -Parameters.MaxDistanceError && lengthDelta <= Parameters.MaxDistanceError) 24 | { 25 | float referenceDelta = FloatAngle.Difference(probe.ReferenceAngle, candidate.ReferenceAngle); 26 | if (referenceDelta <= Parameters.MaxAngleError || referenceDelta >= ComplementaryMaxAngleError) 27 | { 28 | float neighborDelta = FloatAngle.Difference(probe.NeighborAngle, candidate.NeighborAngle); 29 | if (neighborDelta <= Parameters.MaxAngleError || neighborDelta >= ComplementaryMaxAngleError) 30 | return true; 31 | } 32 | } 33 | return false; 34 | } 35 | static List Coverage(EdgeShape edge) 36 | { 37 | int minLengthBin = (edge.Length - Parameters.MaxDistanceError) / Parameters.MaxDistanceError; 38 | int maxLengthBin = (edge.Length + Parameters.MaxDistanceError) / Parameters.MaxDistanceError; 39 | int angleBins = (int)Math.Ceiling(2 * Math.PI / Parameters.MaxAngleError); 40 | int minReferenceBin = (int)(FloatAngle.Difference(edge.ReferenceAngle, Parameters.MaxAngleError) / Parameters.MaxAngleError); 41 | int maxReferenceBin = (int)(FloatAngle.Add(edge.ReferenceAngle, Parameters.MaxAngleError) / Parameters.MaxAngleError); 42 | int endReferenceBin = (maxReferenceBin + 1) % angleBins; 43 | int minNeighborBin = (int)(FloatAngle.Difference(edge.NeighborAngle, Parameters.MaxAngleError) / Parameters.MaxAngleError); 44 | int maxNeighborBin = (int)(FloatAngle.Add(edge.NeighborAngle, Parameters.MaxAngleError) / Parameters.MaxAngleError); 45 | int endNeighborBin = (maxNeighborBin + 1) % angleBins; 46 | var coverage = new List(); 47 | for (int lengthBin = minLengthBin; lengthBin <= maxLengthBin; ++lengthBin) 48 | for (int referenceBin = minReferenceBin; referenceBin != endReferenceBin; referenceBin = (referenceBin + 1) % angleBins) 49 | for (int neighborBin = minNeighborBin; neighborBin != endNeighborBin; neighborBin = (neighborBin + 1) % angleBins) 50 | coverage.Add((referenceBin << 24) + (neighborBin << 16) + lengthBin); 51 | return coverage; 52 | } 53 | public static Dictionary> Build(FingerprintTemplate template) 54 | { 55 | var map = new Dictionary>(); 56 | for (int reference = 0; reference < template.Minutiae.Length; ++reference) 57 | for (int neighbor = 0; neighbor < template.Minutiae.Length; ++neighbor) 58 | if (reference != neighbor) 59 | { 60 | var edge = new IndexedEdge(template.Minutiae, reference, neighbor); 61 | foreach (int hash in Coverage(edge.Shape)) 62 | { 63 | List list; 64 | if (!map.TryGetValue(hash, out list)) 65 | map[hash] = list = new List(); 66 | list.Add(edge); 67 | } 68 | } 69 | // https://sourceafis.machinezoo.com/transparency/edge-hash 70 | FingerprintTransparency.Current.LogEdgeHash(map); 71 | return map; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Primitives/IntPoint.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace SourceAFIS.Engine.Primitives 6 | { 7 | readonly struct IntPoint : IEquatable, IComparable 8 | { 9 | public static readonly IntPoint Zero = new IntPoint(); 10 | public static readonly IntPoint[] EdgeNeighbors = new IntPoint[] { 11 | new IntPoint(0, -1), 12 | new IntPoint(-1, 0), 13 | new IntPoint(1, 0), 14 | new IntPoint(0, 1) 15 | }; 16 | public static readonly IntPoint[] CornerNeighbors = new IntPoint[] { 17 | new IntPoint(-1, -1), 18 | new IntPoint(0, -1), 19 | new IntPoint(1, -1), 20 | new IntPoint(-1, 0), 21 | new IntPoint(1, 0), 22 | new IntPoint(-1, 1), 23 | new IntPoint(0, 1), 24 | new IntPoint(1, 1) 25 | }; 26 | 27 | public readonly int X; 28 | public readonly int Y; 29 | 30 | public int Area => X * Y; 31 | public int LengthSq => Integers.Sq(X) + Integers.Sq(Y); 32 | 33 | public IntPoint(int x, int y) 34 | { 35 | X = x; 36 | Y = y; 37 | } 38 | 39 | public static IntPoint operator +(IntPoint left, IntPoint right) => new IntPoint(left.X + right.X, left.Y + right.Y); 40 | public static IntPoint operator -(IntPoint left, IntPoint right) => new IntPoint(left.X - right.X, left.Y - right.Y); 41 | public static IntPoint operator -(IntPoint point) => new IntPoint(-point.X, -point.Y); 42 | public static bool operator ==(IntPoint left, IntPoint right) => left.X == right.X && left.Y == right.Y; 43 | public static bool operator !=(IntPoint left, IntPoint right) => left.X != right.X || left.Y != right.Y; 44 | 45 | public override int GetHashCode() => 31 * X + Y; 46 | public bool Equals(IntPoint other) => X == other.X && Y == other.Y; 47 | public override bool Equals(object other) => other is IntPoint p && Equals(p); 48 | public int CompareTo(IntPoint other) 49 | { 50 | int resultY = Y.CompareTo(other.Y); 51 | if (resultY != 0) 52 | return resultY; 53 | return X.CompareTo(other.X); 54 | } 55 | public override string ToString() => $"[{X},{Y}]"; 56 | public ShortPoint ToShort() => new(X, Y); 57 | public bool Contains(IntPoint other) => other.X >= 0 && other.Y >= 0 && other.X < X && other.Y < Y; 58 | public IntPoint[] LineTo(IntPoint to) 59 | { 60 | IntPoint[] result; 61 | var relative = to - this; 62 | if (Math.Abs(relative.X) >= Math.Abs(relative.Y)) 63 | { 64 | result = new IntPoint[Math.Abs(relative.X) + 1]; 65 | if (relative.X > 0) 66 | { 67 | for (int i = 0; i <= relative.X; ++i) 68 | result[i] = new IntPoint(X + i, Y + Doubles.RoundToInt(i * (relative.Y / (double)relative.X))); 69 | } 70 | else if (relative.X < 0) 71 | { 72 | for (int i = 0; i <= -relative.X; ++i) 73 | result[i] = new IntPoint(X - i, Y - Doubles.RoundToInt(i * (relative.Y / (double)relative.X))); 74 | } 75 | else 76 | result[0] = this; 77 | } 78 | else 79 | { 80 | result = new IntPoint[Math.Abs(relative.Y) + 1]; 81 | if (relative.Y > 0) 82 | { 83 | for (int i = 0; i <= relative.Y; ++i) 84 | result[i] = new IntPoint(X + Doubles.RoundToInt(i * (relative.X / (double)relative.Y)), Y + i); 85 | } 86 | else if (relative.Y < 0) 87 | { 88 | for (int i = 0; i <= -relative.Y; ++i) 89 | result[i] = new IntPoint(X - Doubles.RoundToInt(i * (relative.X / (double)relative.Y)), Y - i); 90 | } 91 | else 92 | result[0] = this; 93 | } 94 | return result; 95 | } 96 | 97 | public IEnumerable Iterate() 98 | { 99 | for (int y = 0; y < Y; ++y) 100 | for (int x = 0; x < X; ++x) 101 | yield return new IntPoint(x, y); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /SourceAFIS/FingerprintMatcher.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System; 3 | using System.Collections.Generic; 4 | using SourceAFIS.Engine.Features; 5 | using SourceAFIS.Engine.Matcher; 6 | 7 | namespace SourceAFIS 8 | { 9 | /// Fingerprint template representation optimized for fast 1:N matching. 10 | /// 11 | /// 12 | /// FingerprintMatcher maintains data structures that improve matching speed at the cost of some RAM. 13 | /// It can efficiently match one probe fingerprint to many candidate fingerprints. 14 | /// 15 | /// 16 | /// New matcher is created by passing probe fingerprint template to constructor. 17 | /// Candidate fingerprint templates are then passed one by one to method. 18 | /// 19 | /// 20 | /// SourceAFIS for Java tutorial 21 | /// 22 | public class FingerprintMatcher 23 | { 24 | internal readonly FingerprintTemplate Template; 25 | internal readonly Dictionary> Hash; 26 | 27 | /// Creates fingerprint template representation optimized for fast 1:N matching. 28 | /// 29 | /// 30 | /// Once the probe template is processed, candidate templates can be compared to it 31 | /// by calling . 32 | /// 33 | /// 34 | /// This constructor is expensive in terms of RAM footprint and CPU usage. 35 | /// Initialized FingerprintMatcher should be reused for multiple calls in 1:N matching. 36 | /// 37 | /// 38 | /// Probe fingerprint template to be matched to candidate fingerprints. 39 | /// Thrown when is null. 40 | /// 41 | public FingerprintMatcher(FingerprintTemplate probe) 42 | { 43 | if (probe == null) 44 | throw new ArgumentNullException(nameof(probe)); 45 | Template = probe; 46 | Hash = EdgeHashes.Build(probe); 47 | } 48 | 49 | /// Matches candidate fingerprint to probe fingerprint and calculates similarity score. 50 | /// 51 | /// 52 | /// Candidate fingerprint in parameter is matched to probe fingerprint 53 | /// previously passed to constructor. 54 | /// 55 | /// 56 | /// Returned similarity score is a non-negative number that increases with similarity between probe and candidate fingerprints. 57 | /// Application should compare the score to a threshold with expression (score >= threshold) to arrive at boolean match/non-match decision. 58 | /// Threshold 10 corresponds to FMR (False Match Rate, see Biometric Performance 59 | /// and Confusion matrix) of 10%, threshold 20 to FMR 1%, threshold 30 to FMR 0.1%, and so on. 60 | /// 61 | /// 62 | /// Recommended threshold is 40, which corresponds to FMR 0.01%. 63 | /// Correspondence between threshold and FMR is approximate and varies with quality of fingerprints being matched. 64 | /// Increasing threshold rapidly reduces FMR, but it also slowly increases FNMR (False Non-Match Rate). 65 | /// Threshold must be tailored to the needs of the application. 66 | /// 67 | /// 68 | /// This method is thread-safe. Multiple threads can match candidates against single FingerprintMatcher. 69 | /// 70 | /// 71 | /// Fingerprint template to be matched with probe fingerprint represented by this FingerprintMatcher. 72 | /// Similarity score between probe and candidate fingerprints. 73 | /// Thrown when is null. 74 | public double Match(FingerprintTemplate candidate) 75 | { 76 | if (candidate == null) 77 | throw new ArgumentNullException(nameof(candidate)); 78 | return MatcherEngine.Match(this, candidate); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /SourceAFIS.Tests/Engine/Primitives/BooleanMatrixTest.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using NUnit.Framework; 3 | 4 | namespace SourceAFIS.Engine.Primitives 5 | { 6 | public class BooleanMatrixTest 7 | { 8 | BooleanMatrix m = new BooleanMatrix(4, 5); 9 | 10 | [SetUp] 11 | public void SetUp() 12 | { 13 | for (int x = 0; x < m.Width; ++x) 14 | for (int y = 0; y < m.Height; ++y) 15 | m[x, y] = (x + y) % 2 > 0; 16 | } 17 | [Test] 18 | public void Constructor() 19 | { 20 | Assert.AreEqual(4, m.Width); 21 | Assert.AreEqual(5, m.Height); 22 | } 23 | [Test] 24 | public void ConstructorFromPoint() 25 | { 26 | var m = new BooleanMatrix(new IntPoint(4, 5)); 27 | Assert.AreEqual(4, m.Width); 28 | Assert.AreEqual(5, m.Height); 29 | } 30 | [Test] 31 | public void ConstructorCloning() 32 | { 33 | var m = new BooleanMatrix(this.m); 34 | Assert.AreEqual(4, m.Width); 35 | Assert.AreEqual(5, m.Height); 36 | for (int x = 0; x < m.Width; ++x) 37 | for (int y = 0; y < m.Height; ++y) 38 | Assert.AreEqual(this.m[x, y], m[x, y]); 39 | } 40 | [Test] 41 | public void Size() 42 | { 43 | Assert.AreEqual(4, m.Size.X); 44 | Assert.AreEqual(5, m.Size.Y); 45 | } 46 | [Test] 47 | public void Get() 48 | { 49 | Assert.AreEqual(true, m[1, 4]); 50 | Assert.AreEqual(false, m[3, 1]); 51 | } 52 | [Test] 53 | public void GetAt() 54 | { 55 | Assert.AreEqual(true, m[new IntPoint(3, 2)]); 56 | Assert.AreEqual(false, m[new IntPoint(2, 4)]); 57 | } 58 | [Test] 59 | public void GetFallback() 60 | { 61 | Assert.AreEqual(false, m.Get(0, 0, true)); 62 | Assert.AreEqual(true, m.Get(3, 0, false)); 63 | Assert.AreEqual(false, m.Get(0, 4, true)); 64 | Assert.AreEqual(true, m.Get(3, 4, false)); 65 | Assert.AreEqual(false, m.Get(-1, 4, false)); 66 | Assert.AreEqual(true, m.Get(-1, 4, true)); 67 | Assert.AreEqual(false, m.Get(2, -1, false)); 68 | Assert.AreEqual(true, m.Get(4, 2, true)); 69 | Assert.AreEqual(false, m.Get(2, 5, false)); 70 | } 71 | [Test] 72 | public void GetAtFallback() 73 | { 74 | Assert.AreEqual(false, m.Get(new IntPoint(0, 0), true)); 75 | Assert.AreEqual(true, m.Get(new IntPoint(3, 0), false)); 76 | Assert.AreEqual(false, m.Get(new IntPoint(0, 4), true)); 77 | Assert.AreEqual(true, m.Get(new IntPoint(3, 4), false)); 78 | Assert.AreEqual(false, m.Get(new IntPoint(-1, 2), false)); 79 | Assert.AreEqual(true, m.Get(new IntPoint(-1, 2), true)); 80 | Assert.AreEqual(false, m.Get(new IntPoint(0, -1), false)); 81 | Assert.AreEqual(true, m.Get(new IntPoint(4, 0), true)); 82 | Assert.AreEqual(false, m.Get(new IntPoint(0, 5), false)); 83 | } 84 | [Test] 85 | public void Set() 86 | { 87 | Assert.AreEqual(false, m[2, 4]); 88 | m[2, 4] = true; 89 | Assert.AreEqual(true, m[2, 4]); 90 | } 91 | [Test] 92 | public void SetAt() 93 | { 94 | Assert.AreEqual(true, m[1, 2]); 95 | m[new IntPoint(1, 2)] = false; 96 | Assert.AreEqual(false, m[1, 2]); 97 | } 98 | [Test] 99 | public void Invert() 100 | { 101 | m.Invert(); 102 | Assert.AreEqual(true, m[0, 0]); 103 | Assert.AreEqual(false, m[3, 0]); 104 | Assert.AreEqual(true, m[0, 4]); 105 | Assert.AreEqual(false, m[3, 4]); 106 | Assert.AreEqual(true, m[1, 3]); 107 | Assert.AreEqual(false, m[2, 1]); 108 | } 109 | [Test] 110 | public void Merge() 111 | { 112 | Assert.AreEqual(true, m[3, 2]); 113 | var o = new BooleanMatrix(4, 5); 114 | for (int x = 0; x < m.Width; ++x) 115 | for (int y = 0; y < m.Height; ++y) 116 | o[x, y] = x < 2 && y < 3; 117 | m.Merge(o); 118 | Assert.AreEqual(true, m[0, 0]); 119 | Assert.AreEqual(true, m[1, 2]); 120 | Assert.AreEqual(false, m[1, 3]); 121 | Assert.AreEqual(true, m[3, 2]); 122 | for (int x = 0; x < m.Width; ++x) 123 | for (int y = 0; y < m.Height; ++y) 124 | Assert.AreEqual((x + y) % 2 > 0 || x < 2 && y < 3, m[x, y]); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /SourceAFIS/Engine/Extractor/Skeletons/BinaryThinning.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using SourceAFIS.Engine.Configuration; 3 | using SourceAFIS.Engine.Features; 4 | using SourceAFIS.Engine.Primitives; 5 | 6 | namespace SourceAFIS.Engine.Extractor.Skeletons 7 | { 8 | static class BinaryThinning 9 | { 10 | enum NeighborhoodType 11 | { 12 | Skeleton, 13 | Ending, 14 | Removable 15 | } 16 | static NeighborhoodType[] NeighborhoodTypes() 17 | { 18 | var types = new NeighborhoodType[256]; 19 | for (uint mask = 0; mask < 256; ++mask) 20 | { 21 | bool TL = (mask & 1) != 0; 22 | bool TC = (mask & 2) != 0; 23 | bool TR = (mask & 4) != 0; 24 | bool CL = (mask & 8) != 0; 25 | bool CR = (mask & 16) != 0; 26 | bool BL = (mask & 32) != 0; 27 | bool BC = (mask & 64) != 0; 28 | bool BR = (mask & 128) != 0; 29 | uint count = Integers.PopulationCount(mask); 30 | bool diagonal = !TC && !CL && TL || !CL && !BC && BL || !BC && !CR && BR || !CR && !TC && TR; 31 | bool horizontal = !TC && !BC && (TR || CR || BR) && (TL || CL || BL); 32 | bool vertical = !CL && !CR && (TL || TC || TR) && (BL || BC || BR); 33 | bool end = count == 1; 34 | if (end) 35 | types[mask] = NeighborhoodType.Ending; 36 | else if (!diagonal && !horizontal && !vertical) 37 | types[mask] = NeighborhoodType.Removable; 38 | } 39 | return types; 40 | } 41 | static bool IsFalseEnding(BooleanMatrix binary, IntPoint ending) 42 | { 43 | foreach (var relativeNeighbor in IntPoint.CornerNeighbors) 44 | { 45 | var neighbor = ending + relativeNeighbor; 46 | if (binary[neighbor]) 47 | { 48 | int count = 0; 49 | foreach (var relative2 in IntPoint.CornerNeighbors) 50 | if (binary.Get(neighbor + relative2, false)) 51 | ++count; 52 | return count > 2; 53 | } 54 | } 55 | return false; 56 | } 57 | public static BooleanMatrix Thin(BooleanMatrix input, SkeletonType type) 58 | { 59 | var neighborhoodTypes = NeighborhoodTypes(); 60 | var size = input.Size; 61 | var mutable = new BooleanMatrix(size); 62 | for (int y = 1; y < size.Y - 1; ++y) 63 | for (int x = 1; x < size.X - 1; ++x) 64 | mutable[x, y] = input[x, y]; 65 | var thinned = new BooleanMatrix(size); 66 | bool removedAnything = true; 67 | for (int i = 0; i < Parameters.ThinningIterations && removedAnything; ++i) 68 | { 69 | removedAnything = false; 70 | for (int evenY = 0; evenY < 2; ++evenY) 71 | for (int evenX = 0; evenX < 2; ++evenX) 72 | for (int y = 1 + evenY; y < size.Y - 1; y += 2) 73 | for (int x = 1 + evenX; x < size.X - 1; x += 2) 74 | if (mutable[x, y] && !thinned[x, y] && !(mutable[x, y - 1] && mutable[x, y + 1] && mutable[x - 1, y] && mutable[x + 1, y])) 75 | { 76 | uint neighbors = (mutable[x + 1, y + 1] ? 128u : 0u) 77 | | (mutable[x, y + 1] ? 64u : 0u) 78 | | (mutable[x - 1, y + 1] ? 32u : 0u) 79 | | (mutable[x + 1, y] ? 16u : 0u) 80 | | (mutable[x - 1, y] ? 8u : 0u) 81 | | (mutable[x + 1, y - 1] ? 4u : 0u) 82 | | (mutable[x, y - 1] ? 2u : 0u) 83 | | (mutable[x - 1, y - 1] ? 1u : 0u); 84 | if (neighborhoodTypes[neighbors] == NeighborhoodType.Removable 85 | || neighborhoodTypes[neighbors] == NeighborhoodType.Ending 86 | && IsFalseEnding(mutable, new IntPoint(x, y))) 87 | { 88 | removedAnything = true; 89 | mutable[x, y] = false; 90 | } 91 | else 92 | thinned[x, y] = true; 93 | } 94 | } 95 | // https://sourceafis.machinezoo.com/transparency/thinned-skeleton 96 | FingerprintTransparency.Current.Log(type.Prefix() + "thinned-skeleton", thinned); 97 | return thinned; 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /SourceAFIS.Tests/Engine/Primitives/IntPointTest.cs: -------------------------------------------------------------------------------- 1 | // Part of SourceAFIS for .NET: https://sourceafis.machinezoo.com/net 2 | using System.Collections.Generic; 3 | using NUnit.Framework; 4 | 5 | namespace SourceAFIS.Engine.Primitives 6 | { 7 | public class IntPointTest 8 | { 9 | [Test] 10 | public void Constructor() 11 | { 12 | IntPoint p = new IntPoint(2, 3); 13 | Assert.AreEqual(2, p.X); 14 | Assert.AreEqual(3, p.Y); 15 | } 16 | [Test] 17 | public void Area() => Assert.AreEqual(6, new IntPoint(2, 3).Area); 18 | [Test] 19 | public void LengthSq() 20 | { 21 | Assert.AreEqual(5 * 5, new IntPoint(3, 4).LengthSq); 22 | Assert.AreEqual(5 * 5, new IntPoint(-3, -4).LengthSq); 23 | } 24 | [Test] 25 | public void Contains() 26 | { 27 | IntPoint p = new IntPoint(3, 4); 28 | Assert.IsTrue(p.Contains(new IntPoint(1, 1))); 29 | Assert.IsTrue(p.Contains(new IntPoint(0, 0))); 30 | Assert.IsTrue(p.Contains(new IntPoint(2, 3))); 31 | Assert.IsTrue(p.Contains(new IntPoint(0, 3))); 32 | Assert.IsTrue(p.Contains(new IntPoint(2, 0))); 33 | Assert.IsFalse(p.Contains(new IntPoint(-1, 1))); 34 | Assert.IsFalse(p.Contains(new IntPoint(1, -1))); 35 | Assert.IsFalse(p.Contains(new IntPoint(-2, -3))); 36 | Assert.IsFalse(p.Contains(new IntPoint(1, 4))); 37 | Assert.IsFalse(p.Contains(new IntPoint(3, 1))); 38 | Assert.IsFalse(p.Contains(new IntPoint(1, 7))); 39 | Assert.IsFalse(p.Contains(new IntPoint(5, 1))); 40 | Assert.IsFalse(p.Contains(new IntPoint(8, 9))); 41 | } 42 | [Test] 43 | public void Equality() 44 | { 45 | Assert.IsTrue(new IntPoint(2, 3) == new IntPoint(2, 3)); 46 | Assert.IsFalse(new IntPoint(2, 3) != new IntPoint(2, 3)); 47 | Assert.IsFalse(new IntPoint(2, 3) == new IntPoint(0, 3)); 48 | Assert.IsTrue(new IntPoint(2, 3) != new IntPoint(0, 3)); 49 | Assert.IsFalse(new IntPoint(2, 3) == new IntPoint(2, 0)); 50 | Assert.IsTrue(new IntPoint(2, 3) != new IntPoint(2, 0)); 51 | } 52 | [Test] 53 | public void Equals() 54 | { 55 | Assert.IsTrue(new IntPoint(2, 3).Equals(new IntPoint(2, 3))); 56 | Assert.IsFalse(new IntPoint(2, 3).Equals(new IntPoint(0, 3))); 57 | Assert.IsFalse(new IntPoint(2, 3).Equals(new IntPoint(2, 0))); 58 | Assert.IsFalse(new IntPoint(2, 3).Equals(null)); 59 | Assert.IsFalse(new IntPoint(2, 3).Equals(1)); 60 | } 61 | [Test] 62 | public void HashCode() 63 | { 64 | Assert.AreEqual(new IntPoint(2, 3).GetHashCode(), new IntPoint(2, 3).GetHashCode()); 65 | Assert.AreNotEqual(new IntPoint(2, 3).GetHashCode(), new IntPoint(-2, 3).GetHashCode()); 66 | Assert.AreNotEqual(new IntPoint(2, 3).GetHashCode(), new IntPoint(2, -3).GetHashCode()); 67 | } 68 | [Test] 69 | public void EdgeNeighbors() 70 | { 71 | var s = new HashSet(); 72 | foreach (var n in IntPoint.EdgeNeighbors) 73 | { 74 | s.Add(n); 75 | Assert.AreEqual(1, n.LengthSq); 76 | } 77 | Assert.AreEqual(4, s.Count); 78 | } 79 | [Test] 80 | public void CornerNeighbors() 81 | { 82 | var s = new HashSet(); 83 | foreach (var n in IntPoint.CornerNeighbors) 84 | { 85 | s.Add(n); 86 | Assert.IsTrue(n.LengthSq == 1 || n.LengthSq == 2); 87 | } 88 | Assert.AreEqual(8, s.Count); 89 | } 90 | [Test] 91 | public void Iterate() 92 | { 93 | var l = new List(); 94 | foreach (var p in new IntPoint(2, 3).Iterate()) 95 | l.Add(p); 96 | Assert.AreEqual(new[] { new IntPoint(0, 0), new IntPoint(1, 0), new IntPoint(0, 1), new IntPoint(1, 1), new IntPoint(0, 2), new IntPoint(1, 2) }, l); 97 | foreach (var p in new IntPoint(0, 3).Iterate()) 98 | Assert.Fail(); 99 | foreach (var p in new IntPoint(3, 0).Iterate()) 100 | Assert.Fail(); 101 | foreach (var p in new IntPoint(-1, 3).Iterate()) 102 | Assert.Fail(); 103 | foreach (var p in new IntPoint(3, -1).Iterate()) 104 | Assert.Fail(); 105 | } 106 | [Test] 107 | public void lineTo() 108 | { 109 | CheckLineTo(2, 3, 2, 3, 2, 3); 110 | CheckLineTo(2, 3, 1, 4, 2, 3, 1, 4); 111 | CheckLineTo(2, 3, -1, 3, 2, 3, 1, 3, 0, 3, -1, 3); 112 | CheckLineTo(-1, 2, 0, -1, -1, 2, -1, 1, 0, 0, 0, -1); 113 | CheckLineTo(1, 1, 3, 7, 1, 1, 1, 2, 2, 3, 2, 4, 2, 5, 3, 6, 3, 7); 114 | CheckLineTo(1, 3, 6, 1, 1, 3, 2, 3, 3, 2, 4, 2, 5, 1, 6, 1); 115 | } 116 | void CheckLineTo(int x1, int y1, int x2, int y2, params int[] p) 117 | { 118 | var l = new IntPoint[p.Length / 2]; 119 | for (int i = 0; i < l.Length; ++i) 120 | l[i] = new IntPoint(p[2 * i], p[2 * i + 1]); 121 | Assert.AreEqual(l, new IntPoint(x1, y1).LineTo(new IntPoint(x2, y2))); 122 | } 123 | [Test] 124 | public void ToStringReadable() => Assert.AreEqual("[2,3]", new IntPoint(2, 3).ToString()); 125 | } 126 | } 127 | --------------------------------------------------------------------------------