SourceAFIS for Java

SourceAFIS » Java

SourceAFIS for Java is a pure Java port of SourceAFIS, an algorithm for recognition of human fingerprints. It can compare two fingerprints 1:1 or search a large database 1:N for matching fingerprint. It takes fingerprint images on input and produces similarity score on output. Similarity score is then compared to customizable match threshold.

Download

Get SourceAFIS for Java from Maven Central:

Maven tool
<dependency>
    <groupId>com.machinezoo.sourceafis</groupId>
    <artifactId>sourceafis</artifactId>
    <version>3.18.1</version>
</dependency>

Or clone sources from GitHub or Bitbucket. Don't forget to configure your build for Java 11+. Last version compatible with Java 8 was 3.13.0. On Android, SourceAFIS requires Android API level 24+ (tested with SourceAFIS 3.14.2). Sources and binaries are distributed under Apache License 2.0.

If your project is a Java module, add the following declaration to your module-info.java:

requires com.machinezoo.sourceafis;

Input fingerprints

In order to use SourceAFIS, you need fingerprints to process. Fingerprints come in the form of either fingerprint images or fingerprint templates. If you don't know what templates are, don't worry. We will return to templates later. If you just want to try out SourceAFIS, you can download one of the smaller fingerprint datasets, for example FVC2002 DB1 B. In real application, you would get images from fingerprint sensor via sensor manufacturer's SDK.

Standard image formats

Fingerprint images in standard formats (PNG, JPEG, ...) are the easiest to use:

var encoded = Files.readAllBytes(Paths.get("fingerprint.png"));
var decoded = new FingerprintImage(encoded);

FingerprintImage holds decoded fingerprint image. We will use it later.

Grayscale images

Many sensors write fingerprint image into raw grayscale buffer. FingerprintImage accepts raw grayscale directly:

var raw = Files.readAllBytes(Paths.get("fingerprint.gray"));
int width = 300, height = 400;
var decoded = new FingerprintImage(width, height, raw);

Width and height of the image can be found in sensor documentation. Read javadoc for FingerprintImage(int, int, byte[]) constructor for detailed input specification. If your sensor's buffer has different format, you might have to transform the image first.

Image DPI

Both of the above FingerprintImage constructors have overloads taking FingerprintImageOptions. Currently the only option is image DPI.

It is recommended to specify image DPI via dpi(double), because SourceAFIS is not scale-invariant. You can find image DPI in sensor documentation. 500dpi is the most common resolution. SourceAFIS ignores any DPI stored in images themselves.

var options = new FingerprintImageOptions()
    .dpi(500);
var encoded = Files.readAllBytes(Paths.get("fingerprint.png"));
var decoded = new FingerprintImage(encoded, options);

Foreign fingerprint templates (ISO, ANSI)

Although SourceAFIS is designed to process fingerprint images, it will also accept fingerprint templates in one of the publicly documented formats with implementation in FingerprintIO library. If you are dealing with unknown template format, try to pass it to importTemplate(byte[]) to see whether SourceAFIS can decode it.

var encoded = Files.readAllBytes(Paths.get("fingerprint.tmpl"));
var decoded = FingerprintCompatibility.importTemplate(foreign);

This will produce FingerprintTemplate (see below) directly, skipping FingerprintImage step.

Feature extraction

FingerprintImage stores fingerprint pixel-by-pixel. While such representation preserves all details, it does not allow efficient comparison of fingerprints. FingerprintTemplate represents fingerprint using distinctive features, chiefly ridge endings and bifurcations, collectively called minutiae. Conversion of fingerprint image into template is called feature extraction and in SourceAFIS it's performed by FingerprintTemplate constructor.

var image = new FingerprintImage(Files.readAllBytes(Paths.get("fingerprint.png")));
var template = new FingerprintTemplate(image);

Templates are an obviously lossy representation. Original image cannot be reconstructed from the template. Templates however have the advantage of efficient comparison. They also consume less memory. Feature extraction is an expensive process involving non-trivial image processing. Template captures result of feature extraction, so that it does not have to be repeated.

Comparing two fingerprints

Fingerprint comparison, also called 1:1 matching or verification, takes two fingerprint templates and decides whether they match, i.e. whether they come from the same finger. By convention, the two fingerprints are called probe and candidate. Probe is the freshly captured fingerprint while candidate is the one captured and stored in the past.

var probe = new FingerprintTemplate(
    new FingerprintImage(Files.readAllBytes(Paths.get("probe.png"))));
var candidate = new FingerprintTemplate(
    new FingerprintImage(Files.readAllBytes(Paths.get("candidate.png"))));

Probe is used to construct FingerprintMatcher, which is just another representation of the fingerprint. The reason for having this separate representation will become apparent later. Candidate is passed to its match(FingerprintTemplate) method.

var matcher = new FingerprintMatcher(probe);
double similarity = matcher.match(candidate);

Variable similarity now holds similarity score of the two fingerprints. Similarity score is an approximate measure of similarity of two fingerprints. The higher it is, the more likely is the match.

Similarity score is necessarily probabilistic. You can never be sure of a match no matter how high the score is. It is however possible to come up with a reasonable threshold, beyond which it is sufficiently certain that the two fingerprints match.

double threshold = 40;
boolean matches = similarity >= threshold;

Certainty in fingerprint recognition is measured by FMR: false match rate. FMR is the frequency with which the system incorrectly recognizes non-matching fingerprints as matching. Threshold 40 corresponds to FMR 0.01%. Applications can increase the threshold to get exponentially lower FMR at the cost of slightly higher FNMR: false non-match rate. In the end, choice of threshold is application-dependent, but 40 is a good starting point.

Searching fingerprint database

Searching fingerprint database, also called 1:N matching or identification, involves comparing single probe fingerprint to a number of candidate fingerprints.

In 1:N matching, it is no longer sufficient to return similarity score. We need to identify the particular person, called subject in biometric jargon, whose fingerprint was matched. For the purpose of this tutorial, we will define a class that describes subject with single fingerprint.

record Subject(int id, String name, FingerprintTemplate template) {}

Although most applications will persist their database of fingerprints to disk, efficient identification requires caching all candidate fingerprints in memory as FingerprintTemplate objects. As far as SourceAFIS is concerned, this collection of in-memory templates comprises the database. Persistence to on-disk database is application's responsibility.

List<Subject> candidates = loadDB(); // app-specific database fetch

We can now define a method that takes the probe fingerprint and a list of candidate fingerprints and returns the best match.

Subject identify(FingerprintTemplate probe, Iterable<Subject> candidates) {
    var matcher = new FingerprintMatcher(probe);
    Subject match = null;
    double max = Double.NEGATIVE_INFINITY;
    for (var candidate : candidates) {
        double similarity = matcher.match(candidate.template());
        if (similarity > max) {
            max = similarity;
            match = candidate;
        }
    }
    double threshold = 40;
    return max >= threshold ? match : null;
}

It is important to call FingerprintMatcher(FingerprintTemplate) constructor only once, because it is an expensive operation. It builds in-memory data structures that speed up matching. Individual calls to match(FingerprintTemplate) method are relatively fast.

You might be wondering why SourceAFIS does not provide such search method in the API. Aside from making it easier to associate templates with application-defined user identities, keeping the search loop on application side allows applications to customize the loop. Examples of such customizations include:

Persisting fingerprint templates

It would be unreasonably expensive to recreate all fingerprint templates in the database from fingerprint images every time the application restarts. For this reason, SourceAFIS provides a way to persist fingerprint templates.

var template = new FingerprintTemplate(
    new FingerprintImage(Files.readAllBytes(Paths.get("fingerprint.png"))));
byte[] serialized = template.toByteArray();

Method toByteArray() produces binary representation of the template that can be stored in a database to speed up application restarts. When application starts, it deserializes templates using FingerprintTemplate(byte[]) constructor instead of recreating them from fingerprint images.

var serialized = Files.readAllBytes(Paths.get("fingerprint.cbor"));
var template = new FingerprintTemplate(serialized);

Serialized templates are not a substitute for fingerprint images. They are tied to specific SourceAFIS version. In order to allow SourceAFIS upgrades, applications should always store original fingerprint images and treat serialized templates as a temporary compute cache.

Algorithm transparency data

Aside from the obvious outputs (templates, similarity score), SourceAFIS algorithm can log large amounts of data as part of algorithm transparency. In order to receive transparency data, define transparency data consumer that implements FingerprintTransparency. In this tutorial, we will define consumer that just outputs a list of transparency data it receives.

class TransparencyContents extends FingerprintTransparency {
    @Override
    public void take(String key, String mime, byte[] data) {
        System.out.printf("%,9d B  %-17s %s\n", data.length, mime, key);
    }
}

Transparency data logging is disabled by default. To enable it, instantiate your transparency data consumer while SourceAFIS algorithm is running on the thread.

var probeImage = Files.readAllBytes(Paths.get("probe.png"));
var candidateImage = Files.readAllBytes(Paths.get("candidate.png"));

// Actions that shouldn't be logged must run outside the try-with-resources block.
var candidate = new FingerprintTemplate(new FingerprintImage(candidateImage));

// TransparencyContents is the transparency data consumer we defined above.
// It is active inside the try-with-resources block.
try (var transparency = new TransparencyContents()) {
    // Log feature extractor activity.
    var probe = new FingerprintTemplate(new FingerprintImage(probeImage));
    // Log probe preparation.
    var matcher = new FingerprintMatcher(probe);
    // Log comparison with candidate.
    matcher.match(candidate);
}

The above code should produce an overview of available transparency data:

        6 B  text/plain        version
1,306,040 B  application/cbor  decoded-image
1,306,040 B  application/cbor  scaled-image
      371 B  application/cbor  blocks
  167,088 B  application/cbor  histogram
  181,067 B  application/cbor  smoothed-histogram
    5,878 B  application/cbor  contrast
        .           .             .
        .           .             .
        .           .             .
one.util.streamex.StreamEx@4f6dde3f

Transparency data logging is computationally expensive. You can limit its performance impact by overriding FingerprintTransparency.accepts(String) and restricting logging to what you need. On the other hand, if you are capturing all data anyway, you can use predefined transparency consumer FingerprintTransparency.zip(OutputStream) to create transparency ZIP.

You can parse the data yourself using information on algorithm transparency subpages or you can use parsers included in library SourceAFIS Transparency for Java. If all you need is visualization, you can use SourceAFIS Visualization for Java.

Next steps