A self-contained correlation-based fingerprint matcher for small-area press sensors, written in plain C99 with no dependencies beyond libm.
./install.sh # build, self-check, install into ~/.local
fpmatch pair A.pgm B.pgm # one similarity score on stdout
It exists because on sensors this small the open-source stack does not merely
match badly — it declines to match at all. libfprint's only matcher for an
image device is NBIS bozorth3, which hard-returns a zero score unless both
prints yield at least MIN_COMPUTABLE_BOZORTH_MINUTIAE = 10 minutiae
(libfprint/nbis/include/bozorth.h:122). A 144x64 press capture yields 1 to
10. On the pool measured here every one of 2556 dev and 3144 test pairs
scored exactly 0.0 — EER 50%, indistinguishable from a coin. fpmatch
replaces that with band-limited phase-only correlation, which does not care
how many minutiae there are, and ships a patch that lets libfprint use it
(integration/libfprint/).
Developed against the ELAN 04f3:0c3d pad: 144x64 px, ~7x3 mm, ~500 dpi,
8-bit.
| # | piece | where |
|---|---|---|
| 1 | Press capture, fixed. The ELAN driver was running swipe frame-assembly on a static press, stitching a seamed 216x121 composite out of a 144x64 sensor. The fix takes the press as the press. | a libfprint fork (libfprint-elan-press), not in this repository |
| 2 | fpmatch. Band-limited phase-only correlation. C99, zero dependencies, ~2900 lines of src/ including comments, plain cc and make. | src/, and this whole repo |
| 3 | Coverage-guided enrolment. Wires libfprint's existing fpi_image_device_retry_scan() to a real coverage metric, so enrolment asks for the parts of the finger it does not have yet. | integration/libfprint/COVERAGE.md |
Two answers, and they disagree. Both are below, because publishing only the flattering one would be the dishonest move.
Held-out test split, TAR at FAR = 0, enrolment = scans 01-14, probes = scans 25-40, 48 genuine and 96 imposter decisions:
| system | test TAR@FAR=0 | FAR |
|---|---|---|
NBIS mindtct+bozorth3 — what libfprint ships | 0.0% (0/48) | 0/96 |
fpmatch, --template images (the default) | 18.8% (9/48) | 0/96 |
fpmatch, --template mosaic (option, off by default) | 25.0% (12/48) | 0/96 |
| ELAN's own engine at its shipped threshold | 39.6% (19/48) | 0/96 |
Paired McNemar on the same 48 probes: fpmatch beats NBIS p = 0.0039; the vendor beats fpmatch-images p = 0.0129; the vendor vs fpmatch-mosaic is p = 0.0654, not significant. Enrolled EER for the default configuration on the held-out split is 39.58% (~39%).
Read on its own, 18.8% says roughly four presses in five are rejected.
The same binary, at the same frozen defaults, driving fprintd and PAM
against the physical reader. Numbers reconstructed from journald after the
fact, so this is an observation of ordinary use and not a designed experiment.
20 attempts, 12 accepted. The last three attempts (best scores 0.2273,
0.1706, 0.1704) were the author deliberately presenting other fingers to
see whether they would get in. Splitting on that:
| attempts | accepted | rate | |
|---|---|---|---|
| genuine (enrolled finger) | 17 | 12 | 70.6% TAR |
| imposter (deliberate other fingers) | 3 | 0 | 0% FAR |
The best imposter scored 0.2273 against a live threshold of 0.2755.
The offline split predicted 16.7% for this configuration. Getting 12 or more accepts out of 17 attempts if the true rate were 16.7% has probability 1.3e-06. Whatever is going on, it is not a lucky run.
The genuine/imposter split is inferred, from the timing in the log plus the author's account of what he was doing. Nobody wrote down "this attempt is finger X" at the time. So here is the conservative reading as well: if you refuse the inference and count all 20 attempts as genuine, that is 12/20 = 60%. Both readings crush the offline prediction by a wide margin.
One of those accepts scored
0.2759against a threshold of0.2755. That is a margin of0.0004, about one part in 700 of the score.The threshold is derived from the dev split of one person's fingers on one ELAN pad. It is per-sensor and per-person, the score scale moves when the matcher's parameters move, and a value that is correct here can be a lockout or a hole somewhere else. Re-derive it for your hardware.
docs/CALIBRATION.mdis the recipe, andintegration/libfprint/DESIGN.mdshows how the shipped0.2755was produced.
Synthetic cross-session benchmarks substantially understate real-world performance on this kind of sensor. The offline protocol pairs an enrolment from one sitting against probes from a different sitting weeks later, chosen without regard to how the finger was placed. A person at a lock screen does something else entirely: they place the finger the way they always place it, they can see the sensor, and if it does not open they simply press again. That gap is worth more than 50 points here.
What this does not mean:
- It is not "reliable". It is one person's finger, on one sensor, at a threshold with a 0.0004 margin, over 20 logged attempts.
- The imposter fingers were the author's own other fingers. That measures finger separation, not a population false-accept rate. No stranger's finger has ever been presented to it.
- The 0% FAR above rests on three imposter attempts. Three.
If you run it, please report your numbers, good or bad. Use the accuracy report issue template. Ten people reporting real figures would do more for this project than any algorithm change, and "useless on my hardware" is as valuable as a good result.
- One subject. Three fingers. 120 captures. Genuine pairs are the same finger; imposter pairs are a different finger of the same person.
- Therefore "FAR = 0" is a finger-separation rate, not a population false accept rate.
- The finest FAR this pool can resolve is 1/96 = 1.04%. "FAR = 0" means "no false accepts in 96 decisions", 95% CI [0, 3.8%] — it does not mean "below 0.1%", and the harness's TAR@FAR=0.1% column is meaningless here.
- No EER in this repository is a certified accuracy figure. There is no standardised corpus behind them (no FVC, no NIST SD), no third-party scoring, and no vitality/spoof testing at all.
- The offline comparison is biased towards fpmatch — its threshold is chosen on the same data it is then scored on, while the vendor's is fixed in a DLL — and fpmatch still loses there.
Full measurement, protocol, significance tests and caveats:
eval/RESULTS.md. The engineering story, including what
to try next: FINDINGS.md.
This section is Dragos writing in the first person. Everything else in this repository is measurement; this part is just how it happened.
It started with a password manager. I wanted my iCloud Keychain available in Zen browser on Linux, which meant writing a little helper to hold the credentials, and I thought it would be nice if my fingerprint reader could unlock it. The reader had always worked in Windows. On Linux it was a coin flip.
Enrolment would complete happily and then verification would refuse me. I
spent a while assuming I had set something up wrong. The moment I stopped
being able to let it go was when I enrolled my index finger, put my middle
finger on the pad, and watched sudo let me straight through. A reader that
rejects the right finger is annoying. A reader that accepts the wrong one is a
different thing, and I wanted to know which of the two problems I actually
had.
Answering that took a long detour. I built an offline Windows 11 VM purely so I could watch how Windows drives the same piece of hardware, on the theory that if Windows could do it, the silicon could do it. What I found was that the sensor does no matching at all. The matching is a software DLL that ships with the driver. That was good news twice over: it meant Linux was not missing some magic chip, and it meant I could run the vendor's matcher under Wine and use it as a measuring stick for anything I wrote. Having a number to beat changed the whole project.
Then came the part nobody writes about. For a long stretch my imposter scores were higher than my genuine ones, which is worse than useless, and I could not see why. I ran a sweep of 133 configurations across every parameter the matcher has, and it found nothing that survived its own noise. One tuning run ate 69 GB of RAM and took my desktop down with it. There were evenings where the honest summary of the day's work was a negative result, and I wrote those down too, because a repository full of only the wins would be a repository I could not trust six months later.
It unlocks my GNOME lock screen now. I still would not call it solved, and the numbers above say exactly how far from solved it is. But my reader works, it works for the right finger, and the whole thing is under three thousand lines of C that builds in under a second. That will do for now.
./install.sh # build + self-check + install into ~/.local
./install.sh /usr/local # or a system prefix you can write to
./install.sh --uninstall # put it back
or by hand, which is the same thing:
make # cc + make, links only -lm
make check # self-test: no data, no framework, no network
sudo make install PREFIX=/usr/local
There is no configure step, nothing to download and no optional dependency. A cold single-threaded build of the whole matcher takes about 0.6 s and produces a 53 KB binary from ~2900 lines of C.
| path | what |
|---|---|
src/ | the matcher: FFT, BLPOC, PGM I/O, template format, mosaic enrolment |
tests/ | make check — assert-based self-test plus a pair==batch consistency check |
install.sh | one-command build, self-check and install (or uninstall) |
reproduce.sh | regenerate every published number from a clean build |
eval/ | the scorer-agnostic evaluation harness, the sweep driver, significance tests |
eval/RESULTS.md | every measured number, with the command that produced it |
FINDINGS.md | what was learned, where the wall is, what to try next |
integration/libfprint/ | the patches, design notes and end-to-end tests that make libfprint use fpmatch |
docs/CALIBRATION.md | calibrating parameters and a threshold for your sensor |
docs/YOUR-OWN-CAPTURES.md | how to build your own capture pool (there is none in this repo) |
docs/MOSAIC-DESIGN.md | the mosaic design note, as written before the result was known |
LICENSING.md | licence, clean-room statement, template-privacy trade-off |
CONTRIBUTING.md | what helps most, and the one rule with no exceptions |
tools/devsep.py | quick genuine/imposter separation summary during development |
There are no fingerprint images in this repository and there never will be.
A fingerprint is biometric data about a specific living person and cannot be
rotated after a leak. .gitignore excludes *.pgm and CI fails a push that
adds one. To reproduce the accuracy numbers you need your own captures — see
docs/YOUR-OWN-CAPTURES.md.
The usual open-source path (NBIS mindtct + bozorth3) extracts minutiae —
ridge endings and bifurcations — and matches their relative geometry. That
needs enough of them, and a ~7x3 mm window does not contain enough. The
failure is not graceful: bozorth3 refuses to score below ten minutiae, so the
result is a constant 0.0 rather than a weak score.
Correlation-based matching is the established alternative for small sensors:
instead of extracting features it compares the images directly, in the
frequency domain, so minutia count never enters into it. The technique
implemented here is band-limited phase-only correlation (BLPOC), after
Ito, Nakajima, Kobayashi, Aoki and Higuchi, "A fingerprint matching algorithm
using phase-only correlation", IEICE Trans. Fundamentals E87-A(3), 2004. It
is implemented from the published description of the method; nothing here is
derived from any vendor binary (see LICENSING.md).
- Both images are preprocessed: sensor fixed-pattern (column) noise removed, local mean/variance normalisation to flatten uneven contact pressure, then global normalisation. Optionally a 2D Hanning window.
- Each is zero-padded into a power-of-two canvas (256x128 by default) and forward-transformed with a vendored radix-2 FFT.
- The cross-phase spectrum
R = A·conj(B) / |A·conj(B)|^alphais formed. Withalpha = 1this is classical phase-only correlation: every frequency is whitened to unit magnitude. Loweralphakeeps some of the amplitude. - Band limiting: only the low/mid frequency rectangle
|u| <= K1,|v| <= K2is kept, as fractions of Nyquist (--k1,--k2). The DC bin and, by default, theu = 0/v = 0axes are dropped — they carry the sensor's stripe and banding artefacts, not ridge structure. - An inverse FFT gives the correlation surface, normalised so two identical
images peak at exactly 1.0 for any
alpha. - The score is the highest peak inside a plausible-translation window; the
peak's position is the estimated translation, reported by
--verbose. - All of that is repeated over a coarse rotation search (default ±20° in 4° steps) and the best score wins. Placement angle varies a lot on a pad this small; without the search, genuine pairs are missed.
--score psr switches from raw peak height to peak-to-sidelobe ratio. It is
much worse here (see the sweep) and is kept only because it is the textbook
alternative.
fpmatch pair A.pgm B.pgm one float on stdout, higher = closer
fpmatch enroll OUT.tmpl A.pgm B.pgm ...
fpmatch verify TMPL.tmpl PROBE.pgm one float, max over the enrolled set
fpmatch batch < pairs.txt 'A.pgm B.pgm' per line -> score per line
fpmatch mosaic PREFIX A.pgm B.pgm ... write the mosaic fragments as PGMs
fpmatch --help
pair and verify print exactly one float and nothing else. Failures go to
stderr with a non-zero exit status — a failure is never reported as a score
of zero. batch exists so an evaluation can score thousands of pairs
without paying process startup and FFT setup per pair; it caches prepared
spectra (--cache N).
A default template is just the enrolled images, and verification is the max pair score over the set. That keeps enrolment lossless, at the cost of template size.
The pad is ~7x3 mm, so each press sees a small arbitrary patch and most same-finger pairs share almost no area. Enrolling more images only buys more lottery tickets. Mosaic enrolment instead registers the enrolled captures to each other — BLPOC already returns the translation, not just a score — and composites them onto larger canvases:
fpmatch --template mosaic enroll gallery.tmpl scan-*.pgm
fpmatch verify gallery.tmpl probe.pgm # the template records its mode
- Every pair among the enrolled captures is scored, keeping the peak and the
recovered
(dx, dy, angle). - Edges above
--reg-threshform a graph; fragments grow outward from the strongest edge, largest first (a maximum spanning forest). - Loop closure is checked. Before a capture is admitted, its implied
placement must agree, within
--loop-tol, with every other confident edge back into the fragment — not just the one it arrived on. Registration errors otherwise propagate silently down a spanning tree. - Overlaps are blended by local ridge contrast rather than a plain mean, and
frame borders are feathered (
--taper), so a smeared press does not wash out a sharp one and butted captures leave no hard seam. - Captures that never register confidently stay as separate fragments.
With nothing registered, every capture is its own fragment and the result
is bit-identical to
--template images— pinned bymake checkand by a--reg-thresh 1.1control run on real data.
Verification locates the probe on each canvas, crops the canvas to that
footprint and scores the crop with the ordinary matcher, so scores stay on
exactly the fpmatch pair scale and thresholds carry over.
It is not the default, and the reason is the honest one. It raised
held-out TAR@FAR=0 from 18.8% to 25.0%, three probes gained, none lost, FAR
unchanged — but all three gained probes are thumb, index and middle did
not move by a single decision, and mosaicking only works on a finger whose
presses overlap enough to register at all. It costs about 2x the verification
time and stores a reconstruction of the finger larger than any capture the
sensor ever took. Turn it on deliberately for large-contact-area enrolments;
do not expect it to fix a finger that will not register.
eval/RESULTS.md §10, eval/results/mosaic-DECISION-RULE.md, and
LICENSING.md#template-privacy for the privacy trade.
Every parameter is a flag with a default, so tuning never needs a recompile.
fpmatch --help prints the current defaults.
| flag | default | what it does |
|---|---|---|
--k1 F | 0.7 | horizontal band limit, fraction of Nyquist |
--k2 F | 0.7 | vertical band limit, fraction of Nyquist |
--hp F | 0 | high-pass cut around DC, fraction of Nyquist |
--alpha F | 0.25 | whitening exponent; 1 = phase-only, 0 = plain cross correlation |
--window 0|1 | 0 | 2D Hanning window before the FFT |
--fpn 0|1|2 | 1 | fixed-pattern noise removal: none / column means / column+row |
--drop-axes 0|1 | 1 | ignore the u=0 and v=0 spectrum axes |
--enhance 0|1 | 1 | local mean/variance ridge normalisation |
--enhance-win N | 11 | window for --enhance, pixels |
--rot-range D | 20 | rotation searched, ± degrees |
--rot-step D | 4 | rotation step in degrees; 0 disables the search |
--max-dx N | 60 | largest plausible horizontal shift, pixels |
--max-dy N | 28 | largest plausible vertical shift, pixels |
--overlap-norm 0|1 | 0 | divide the peak by the overlapping image fraction |
--min-overlap F | 0.25 | reject shifts overlapping less than this |
--score peak|psr | peak | scoring mode |
--psr-radius N | 5 | peak exclusion radius used by psr |
--fft WxH | 256x128 | zero-padded transform size, powers of two |
--cache N | 96 | prepared spectra cached in batch mode |
--template images|mosaic | images | enrolment mode (see above) |
-v | off | peak, PSR, angle and translation on stderr |
Textbook BLPOC settings are chance on this sensor. Measured on the dev split (scans 01-24, 72 images, 828 genuine and 1728 imposter pairs):
| change | pairwise EER | d' |
|---|---|---|
textbook BLPOC (--alpha 1 --window 1 --k1 0.35 --k2 0.35) | 0.490 | 0.26 |
| turn the Hanning window off | 0.469 | 0.31 |
| widen the band to 0.7 | 0.383 | 0.59 |
partial whitening --alpha 0.25 | 0.307 | 0.86 |
| widen the rotation search to ±20° | 0.295 | 0.91 |
Whitening (--alpha) is the single biggest lever. Full phase-only
correlation normalises every retained frequency to unit magnitude, which on
low-contrast captures promotes pure sensor noise to the weight of the ridge
band. Windowing hurt, which is the opposite of the usual advice: on a pad
this small two captures of the same finger usually overlap at an edge, and a
Hanning window attenuates exactly the evidence.
A later stage swept 133 configurations of all 18 parameters over the dev
split by coordinate descent. No configuration beat these defaults by more
than the pool's own noise. The best improved dev pairwise EER from 0.295 to
0.256, but a paired bootstrap over images puts that at −0.038 with a 95% CI of
[−0.089, +0.005], and it accepted four fewer genuine probes at the FAR = 0
operating point. The freeze rule was written down before the sweep finished
and before any contact with the test split; the defaults won by default.
Everything, including which parameters do literally nothing, is in
eval/RESULTS.md §4.
Two properties worth knowing before you tune anything:
pairandbatchreturn bit-identical scores for the same parameters, andmake checkfails the build if they ever diverge (tests/cli-consistency.sh).- Matching is not symmetric: only the second image is rotated, so
pair A Bandpair B Adiffer slightly. Pick a convention and keep it.
These defaults are fitted to one ELAN 04f3:0c3d pad, and the shipped
0.2755 threshold is fitted to one person's fingers on it. --alpha,
--k1/--k2, --enhance-win and the rotation range all depend on ridge pitch
in pixels, sensor noise character and how large a patch a press covers, none
of which transfer. Assume you need your own sweep and your own threshold.
The full recipe, with commands you can paste, is
docs/CALIBRATION.md. In outline:
Collect a pool: one flat directory of 8-bit binary PGM,
label-NN.pgm, several fingers, tens of scans each (docs/YOUR-OWN-CAPTURES.md).Split it by scan number and never look at the held-out half:
python3 eval/evaluate.py --pool POOL --scorer "$PWD/fpmatch" \ --name baseline --split dev --enroll-scans 1-14 --out out/baselineSweep on dev only, with a selection metric declared in advance:
python3 eval/sweep.py --pool POOL --jobs 4 --rounds 3 --out sweep.jsonBootstrap over images, not pairs, before believing any win:
python3 eval/sweep.py --pool POOL --mode list \ --configs candidates.json --bootstrap 400Derive your threshold on dev, then score the held-out split once and report what it says.
If you do this, expect to find what this project found: the dominant limit on
a sensor this small is capture overlap, not the matcher. See FINDINGS.md.
Measured single-threaded on one core of an AMD Ryzen 7 9800X3D
(/usr/bin/time -f "%e %M"), 144x64 captures, frozen defaults:
| operation | wall | peak RSS |
|---|---|---|
fpmatch pair A B (11 rotations, 256x128) | 21 ms | 4.1 MB |
verify against a 14-image gallery, --template images | 0.28 s | 4.1 MB |
verify against a 14-image gallery, --template mosaic | 0.53 s | 10.3 MB |
enroll, --template images (a copy) | <10 ms | 2.3 MB |
enroll, --template mosaic (91 pairwise matches) | 1.8 s | 4.4 MB |
batch, 100 pairs, default --cache 96 | 1.6 s | 53 MB |
make check (whole self-test) | 2.4 s | 38 MB |
| cold single-threaded build | 0.6 s | — |
Template sizes for a 14-image thumb gallery: 129 KB images, 98 KB
mosaic. A full dev evaluation — 2556 pairs — takes 12.9 s at -j 4.
The only memory knob that matters is batch's spectrum cache: it holds
--cache N prepared spectra, so a large N on a large FFT size is the one
way to make this process big. FPMATCH_CACHE_MB caps it in megabytes. Nothing
here needs a GPU, a BLAS, or more than a laptop.
./reproduce.sh # POOL from $POOL or the default path
POOL=/path/to/captures ./reproduce.sh
It cleans, rebuilds, runs the self-checks, runs the harness's own synthetic self-test, and then — only if a capture pool is present — regenerates the evaluations, the comparison plots and the libfprint end-to-end test. With no pool it stops after the data-free stages and says so rather than inventing numbers.
integration/libfprint/ carries a patch against libfprint 1.94.10 that adds
fpmatch as an opt-in, per-driver matcher: the print stays FPI_PRINT_NBIS,
the raw images ride along in the FP3 format's existing expansion slot, and a
driver opts in with one call. A libfprint without fpmatch reads such a print
fine and falls back to bozorth3, and prints enrolled before the change keep
working.
It is exercised end to end through the virtual_image driver with no hardware
(e2e-virtual.sh, 5/5 pass), where the standalone CLI is the oracle and
libfprint's score must agree with it to four decimal places. Beyond that it
has been run live against the physical reader through fprintd and PAM, which
is where the lock-screen numbers at the top of this file come from. That was
ordinary use rather than a controlled test, and it is the only hardware any of
this has ever touched.
Read integration/libfprint/DESIGN.md,
especially the threshold derivation and the "what is NOT verified" list,
before doing anything with it.
The third piece, and the one with the most interesting negative result.
libfprint's enrolment is a fixed number of stages with a per-stage pass/fail,
so a driver cannot say "that press was fine but I still have nothing from the
left side of your finger". ELAN's Windows driver does exactly that, and it is
the single largest difference between the two stacks. The patch in
integration/libfprint/COVERAGE.md wires
the existing fpi_image_device_retry_scan() primitive to a real coverage
metric so the enrolment can ask for the placements it is missing.
It works mechanically, and it did not improve matching: on the sealed
split a guided gallery accepted fewer genuine probes than a count-matched
freeform one (3/48 vs 7/48, p = 0.2891, not significant), and
session-to-session variance turned out to be larger than the effect being
measured. The whole measurement is in
eval/GUIDED-RESULTS.md, pre-registered in
eval/GUIDED-DECISION-RULE.md before the
split was read.
A correlation matcher stores image-like data. The default template is the
enrolled captures themselves; a mosaic template is worse, because it is a
single reconstruction of the finger larger than anything the sensor captured,
and fpmatch mosaic will render it as a PGM for anyone holding the file. That
is a real regression against minutiae templates and it is not solved here,
only documented: LICENSING.md#template-privacy.
No dates. This is one person's evenings, and the order will be driven by what people report.
| item | state |
|---|---|
| Other sensors | Completely untested. Every number here is one ELAN 04f3:0c3d. If fpmatch is useless on your pad, that is a finding and it should be filed. |
| More than one subject | The single largest weakness. One person and three fingers is what stands behind every statistic in this repository, and cross-finger imposters are finger separation rather than population FAR. |
| Per-sensor threshold derivation | Today 0.2755 is a constant with a comment. It should be something a user derives on their own data with a documented command, and eventually something the driver can carry per device. |
| libfprint upstreaming | Intended, and the reason the LGPL option is retained. The open question upstream will ask is template privacy, since this stores image-like data in /var/lib/fprint. |
| Capture, not matching | FINDINGS.md §8 argues the next real win is on the capture side. The matcher has been swept to exhaustion; the sensor has not. |
| Everything else | Driven by the accuracy reports that come in. |
Everything measured, in the order a cold visitor would want it.
| document | what it answers |
|---|---|
FINDINGS.md | The engineering story. Why NBIS refuses to score, why textbook BLPOC is chance, why capture overlap is the real wall, and what to try next. Start here. |
eval/RESULTS.md | Every number with the command that produced it: the sweep, the freeze, the single test run, the vendor comparison, the NBIS baseline, the caveats. |
eval/GUIDED-RESULTS.md | Did coverage-guided enrolment help? Measured on the sealed split. No. |
eval/GUIDED-DECISION-RULE.md | The pre-registration for that experiment, written before the split was read. |
eval/results/mosaic-DECISION-RULE.md | The same discipline for the mosaic stage, including the postscript admitting the rule was too weak. |
integration/libfprint/DESIGN.md | How fpmatch plugs into libfprint, what was rejected and why, how 0.2755 was derived, and what is not verified. |
integration/libfprint/COVERAGE.md | The coverage metric, its policy thresholds, and what libfprint's enrolment API cannot express. |
integration/libfprint/CAPTURE-PROTOCOL.md | What the human is actually asked to do during a guided capture session. |
docs/MOSAIC-DESIGN.md | The mosaic design note as written before the result was known, success criterion included. |
docs/CALIBRATION.md | Calibrating parameters and a threshold for a sensor that is not this one. |
docs/YOUR-OWN-CAPTURES.md | Building a capture pool, since this repository ships none. |
LICENSING.md | Licence split, clean-room statement, provenance audit, template privacy. |
See CONTRIBUTING.md. The short version: never commit,
attach or upload fingerprint images, templates or print stores, not even
your own, because a fingerprint cannot be rotated after a leak. Scores,
counts, thresholds and log lines are all welcome. Pixels are not.
The most useful contribution is almost certainly not code. It is your numbers, from your sensor, measured properly.
fpmatch is free and always will be. If it got your reader working, or if the write-up saved you a weekend, you can say thanks here:
There is no paid tier, no sponsor-only feature and nothing withheld. Reporting your accuracy numbers is worth more to the project than money.
- Bugs, sensor reports and accuracy numbers belong in issues, where the next person with the same hardware can find them.
- Collaboration and commercial enquiries, or getting this working on hardware you cannot share publicly: email Dragos at dragosrobu@icloud.com.
Dual-licensed Apache-2.0 OR LGPL-2.1-or-later, your choice, with one exception.
| what | licence |
|---|---|
the matcher: src/, tests/, eval/, tools/, docs/, build files | Apache-2.0 OR LGPL-2.1-or-later |
integration/libfprint/ | LGPL-2.1-or-later only |
Apache-2.0 is there so anyone can use this, including in a closed product, and it carries a patent grant that matters in a field as encumbered as biometrics. LGPL-2.1-or-later is retained because libfprint is LGPL-2.1-or-later and could not accept an Apache-only contribution, and upstreaming is a goal.
integration/libfprint/ is a derivative work of libfprint and legally
cannot be offered under Apache-2.0, so it stays LGPL-2.1-or-later. If you
vendor fpmatch under Apache-2.0, take src/ and leave integration/ behind.
Full licence texts: LICENSE-APACHE and COPYING,
both byte-identical to their canonical upstream versions and checksum-pinned in
CI. The clean-room statement and the provenance audit are in
LICENSING.md.