Fingerprint Verification in ASP.NET Core with SourceAFIS (1:1 Matching)
1:1 verification vs 1:N identification
First, scope it, because these are two very different problems.
- 1:1 verification answers: is this fingerprint the same finger that was enrolled for this specific user? One probe, one stored template. This is what a fingerprint login does.
- 1:N identification answers: who does this fingerprint belong to, out of everyone in the database? One probe against millions of templates. Different, much harder problem.
This article is only about 1:1. We come back to 1:N at the end.
The mental model
The whole thing is four steps: fingerprint image -> template -> match score -> threshold -> match / no match
- A template is the compact set of features (minutiae) extracted from a fingerprint image. You store the template, never the raw image.
- Matching two templates produces a similarity score (a number).
- You compare that score to a threshold to decide match or no match.
That is it. SourceAFIS does the hard math for the first three; you own the threshold and the plumbing.
Setup
Create an API project and add SourceAFIS:
dotnet new web -n FingerprintDemo
cd FingerprintDemo
dotnet add package SourceAFIS
SourceAFIS v3 does not decode PNG or JPEG for you. It expects raw 8-bit grayscale pixels. So we also add an image library to decode uploads:
dotnet add package SixLabors.ImageSharp
Note: the exact SourceAFIS constructor and options shape have changed across major versions. If a signature below does not match your version, check the SourceAFIS docs, the concepts are identical.
From image to template
Decode the uploaded file to grayscale, then build a template. Tell SourceAFIS the scan resolution (500 DPI is the common default for fingerprint sensors), because it matters for feature extraction.
using SixLabors.ImageSharp ;
using SixLabors.ImageSharp.PixelFormats ;
using SourceAFIS ;
// Decode an uploaded PNG/JPEG into raw 8-bit grayscale pixels.
static ( int width , int height , byte [] pixels ) DecodeGrayscale ( byte [] fileBytes )
{
using var image = Image . Load < L8 >( fileBytes ); // L8 = 8 bits luminance
var pixels = new byte [ image . Width * image . Height ];
image . CopyPixelDataTo ( pixels );
return ( image . Width , image . Height , pixels );
}
// Turn grayscale pixels into a SourceAFIS template.
static FingerprintTemplate Extract ( int width , int height , byte [] grayscale )
{
var options = new FingerprintImageOptions { Dpi = 500 };
var image = new FingerprintImage ( width , height , grayscale , options );
return new FingerprintTemplate ( image );
}
Enrollment: extract once, store the template
On enrollment, extract the template and serialize it to bytes. Store those bytes, tied to the user. Do not store the fingerprint image.
FingerprintTemplate template = Extract ( width , height , grayscale );
byte [] serialized = template . ToByteArray (); // store this in your DB
To rebuild a template later, you just pass the bytes back:
var template = new FingerprintTemplate ( serialized );
Verification: match probe against the enrolled template
At login, extract a template from the freshly scanned finger (the probe), build a matcher from the enrolled template, and get a score.
static double Match ( byte [] enrolledSerialized , FingerprintTemplate probe )
{
var enrolled = new FingerprintTemplate ( enrolledSerialized );
var matcher = new FingerprintMatcher ( enrolled );
return matcher . Match ( probe ); // higher score = more similar
}
Choosing a threshold
The score is meaningless until you compare it to a threshold, and the threshold is a business decision, not a technical constant.
- Raise the threshold: fewer false accepts (someone else's finger matching), but more false rejects (the real user gets denied). Lower FMR, higher FRR.
- Lower the threshold: the opposite.
SourceAFIS calibrates its scores so that a threshold around 40 corresponds to a false match rate of roughly 0.01%. That is a sensible starting point:
const double Threshold = 40 ;
bool isMatch = score >= Threshold ;
But starting point is the key phrase. You tune this on your own data, your own sensors, and your own risk appetite. A payments app and a gym check-in do not want the same threshold.
The minimal API
Putting the thread together with two endpoints. This uses an in-memory dictionary as a stand-in for a database, so it is obviously not production code.
using SourceAFIS ;
var builder = WebApplication . CreateBuilder ( args );
var app = builder . Build ();
// userId -> serialized enrolled template. A real app uses a database.
var store = new Dictionary < string , byte [ ]> ();
static async Task < byte [ ]> ReadBody ( HttpRequest req )
{
using var ms = new MemoryStream ();
await req . Body . CopyToAsync ( ms );
return ms . ToArray ();
}
app . MapPost ( "/enroll/{userId}" , async ( string userId , HttpRequest req ) =>
{
var ( w , h , gray ) = DecodeGrayscale ( await ReadBody ( req ));
var template = Extract ( w , h , gray );
store [ userId ] = template . ToByteArray ();
return Results . Ok ( new { enrolled = true });
});
app . MapPost ( "/verify/{userId}" , async ( string userId , HttpRequest req ) =>
{
if (! store . TryGetValue ( userId , out var enrolled ))
return Results . NotFound ( new { error = "user not enrolled" });
var ( w , h , gray ) = DecodeGrayscale ( await ReadBody ( req ));
var probe = new FingerprintTemplate ( new FingerprintImage ( w , h , gray , new FingerprintImageOptions { Dpi = 500 }));
double score = new FingerprintMatcher ( new FingerprintTemplate ( enrolled )). Match ( probe );
return Results . Ok ( new { score , match = score >= 40 });
});
app . Run ();
Post a fingerprint image to /enroll/alice, then post another scan to /verify/alice, and you get back a score and a match decision. That is a working 1:1 fingerprint verification service.
What this naive version deliberately ignores
The code above is a thread to pull on , not a system. Everything below is what stands between it and something you would put in front of real users:
- Image quality and failed captures: partial, smudged, wet or rotated prints. Production systems score capture quality and ask for a re-scan before they even try to match.
- Liveness and anti-spoofing: the code above would happily match a photocopy or a gelatin fake. Detecting a live finger is a whole field of its own.
- Secure storage: a template is biometric data, some of the most sensitive PII there is. It must be encrypted at rest, access-controlled, and handled under laws like GDPR Article 9 or BIPA. You cannot treat it like a password hash.
- Standards and interoperability: real deployments use standardized templates (ISO/IEC 19794-2) and image formats (WSQ) so that enrollments survive across different sensors and vendors.
- Multiple fingers and modalities: enrolling several fingers, and often fusing fingerprint with face for reliability.
- 1:N at scale: matching one probe against millions of templates is not a
forloop overMatch(). It needs candidate filtering, indexing, and careful throughput engineering, or a single identification takes minutes.
Where this leaves you
For a feature, a fingerprint login for a known user, the thread in this post is genuinely enough. Extract, store, match, threshold. You now understand every step.
Turning that into a real biometric identification system, one that scales to large populations, resists spoofing, stays compliant, and stays fast at 1:N, means putting all of those missing pieces together correctly. That is a different level of work, and its own discipline. But it starts exactly here, with understanding what a template is and what a match score means. If building that kind of system is your problem, this is the foundation to build it on.
Comments
No comments yet. Start the discussion.