pad_train
k-NN presentation attack detection (PAD) for finger vein images. The package answers one question: can a PAD system that is trained on synthetic attack samples reliably tell real attacks from bona fide samples?
It returns APCER, BPCER and ACER as a pandas.DataFrame.
Method
Three sample types are involved:
| Type | Meaning | Source |
|---|---|---|
| (1) | bona fide ("real") | Data_FV_Spoofing_WS2025_26 |
| (2) | real presentation attack ("spoof") | Data_FV_Spoofing_WS2025_26 |
| (3) | synthetic presentation attack (diffusion) | AE_Res-diff, VAE-diff |
Each dataset (PLUS, IDIAP, SCUT) is evaluated with a five-fold cross-validation. The folds partition the subjects, so no person is in training and test at the same time. Only type (1) samples that have a type (2) counterpart are used (balanced sets). Two protocols run on identical folds:
protocol |
Training (4/5 of the subjects) | Test (remaining 1/5) |
|---|---|---|
baseline |
(1) vs. (2) | (1) vs. (2) |
synthetic |
(1) vs. (3) | (1) vs. (2) |
The classifier is a k-nearest-neighbour classifier on top of an exchangeable feature extractor. The reported metrics follow ISO/IEC 30107-3:
- APCER – share of attacks classified as bona fide
- BPCER – share of bona fide samples classified as attack
- ACER – mean of the two
Details worth knowing:
- The synthetic samples carry no subject identity, so they cannot be split by
subject. By default each fold draws as many synthetic samples as it has bona
fide training samples (
balance_synthetic=True), which keeps the training set the same size as in the baseline. Synthetic samples are never part of a test set. - "Has a counterpart" is decided per file name for PLUS and IDIAP (real and spoof captures share their names) and per subject for SCUT.
- The metrics are computed per fold and then averaged.
Installation
From PyPI:
pip install pad-train
Directly from the repository, over SSH or HTTPS. The repository is private, so either way you need access to it:
# SSH (needs an SSH key registered with GitHub)
pip install "git+ssh://git@github.com/Kneidl18/PAD_train.git" # latest main
pip install "git+ssh://git@github.com/Kneidl18/PAD_train.git@1.0.0" # a release
# HTTPS (uses your git credential helper, or asks for user name and access token)
pip install "git+https://github.com/Kneidl18/PAD_train.git" # latest main
pip install "git+https://github.com/Kneidl18/PAD_train.git@1.0.0" # a release
For local development:
git clone git@github.com:Kneidl18/PAD_train.git
cd PAD_train
conda env create -f environment.yml # installs the package in editable mode
conda activate pad_train
Data
The three archives (about 3 GB in total) are downloaded on demand into the data root and removed after extraction:
<data root>/
├── Data_FV_Spoofing_WS2025_26/ # types (1) and (2)
├── AE_Res-diff/ # type (3), residual auto encoder
└── VAE-diff/ # type (3), variational auto encoder
The data root defaults to ./data (relative to the working directory). Change
it in Python, through the environment, or on the command line:
from pathlib import Path
import pad_train
pad_train.settings.data_root = Path("/mnt/datasets/finger_vein")
export PAD_TRAIN_DATA_ROOT=/mnt/datasets/finger_vein
pad-train --data-root /mnt/datasets/finger_vein download
All paths, URLs and dataset layouts live in
src/pad_train/config.py.
Fetching happens in one of two ways:
- Explicitly:
pad-train download(orpad_train.download_data()). - Implicitly:
load_data()andevaluate()download whatever is missing and simply use what is already there. Passdownload=Falseto forbid it.
pipcannot run code at install time for wheels, so there is nopip install --download-datasets. Runpad-train downloadafter installing.
Usage
import pad_train
results = pad_train.evaluate(extractors=["fourier", "haralick"])
print(results)
dataset extractor protocol train_attack APCER BPCER ACER
PLUS fourier baseline spoof ...
PLUS fourier synthetic AE_Res ...
PLUS fourier synthetic VAE ...
...
train_attack names the attack samples used for training: spoof for the
baseline, otherwise the synthetic source. All rates are fractions in [0, 1].
Useful arguments of evaluate():
| Argument | Default | Meaning |
|---|---|---|
datasets |
all | e.g. ["PLUS"] |
extractors |
("fourier",) |
registered names or callables |
synthetic |
all | e.g. ["VAE"] |
protocols |
both | "baseline" and/or "synthetic" |
n_neighbors |
per dataset | k of the k-NN (PLUS 6, IDIAP 1, SCUT 1) |
balance_synthetic |
True |
sub-sample the synthetic training samples |
per_fold |
False |
one row per fold instead of the mean |
download |
True |
fetch missing data |
Lower-level building blocks: load_data() returns the images of one dataset,
evaluate_dataset() evaluates images, cross_validate() evaluates precomputed
feature matrices.
The same from the command line:
pad-train evaluate --datasets PLUS IDIAP --extractors fourier haralick --output results.csv
pad-train evaluate --help
Feature extractors
A feature extractor is any callable that maps a grayscale image (2-D uint8
array) to a 1-D feature vector whose length does not depend on the image size.
| Name | Description |
|---|---|
fourier |
log energy of the magnitude spectrum in radial bands, thirds per dataset |
haralick |
13 Haralick texture statistics, averaged over the four directions |
Fourier settings
fourier uses only the magnitude of the spectrum; the phase is discarded. The
30 bands are grouped into thirds (1 = low, 2 = middle, 3 = high frequencies),
and each dataset uses its own selection:
| Dataset | fourier_thirds |
Bands |
|---|---|---|
| PLUS | (1, 2, 3) |
0–29 |
| IDIAP | (1, 2) |
0–19 |
| SCUT | (1,) |
0–9 |
evaluate() and the command line apply these automatically. To change them,
edit fourier_thirds in _default_datasets() in
src/pad_train/config.py, or override them at run time:
pad_train.settings.datasets["SCUT"].fourier_thirds = (1, 2)
Adding an extractor
Adding one takes a single decorator, in this package
(src/pad_train/features/) or in your own code:
import numpy as np
import pad_train
@pad_train.register_extractor("mean_std")
def mean_std(image):
return np.array([image.mean(), image.std()])
# a parametrised variant of a built-in one: the 10 lowest frequency bands
pad_train.register_extractor("fourier_low", pad_train.make_fourier_extractor(bands=range(10)))
pad_train.evaluate(extractors=["fourier", "fourier_low", "mean_std"])
Additional synthetic sources
Any folder with the layout <path>/<dataset>/spoof/samples/*.png can be used
as a source of type (3) samples, for example post-processed versions of the
provided ones:
from pathlib import Path
from pad_train import DataSource, evaluate, settings
settings.synthetic_sources["VAE_cleaned"] = DataSource("VAE_cleaned", Path("/data/VAE_cleaned"))
evaluate(synthetic=["VAE", "VAE_cleaned"])
The dataset folder names are PLUS_matched, IDIAP and SCUT.
Development
ruff format . # formatting
ruff check . # linting (annotations and docstrings included)
mypy # static type checking
pytest # tests; they generate their own toy data, no download needed
Releasing
- Set the new
versioninpyproject.toml, merge it tomain. - Publish a GitHub release on
mainwhose tag is exactly that version (e.g.1.0.0).
The Publish to PyPI workflow then checks,
builds and uploads the package.
Contributing
Contributions are welcome through pull requests on GitHub:
- Fork the repository, or create a branch if you have write access, and
branch off
dev. - Make your change. New feature extractors go into
src/pad_train/features/and need a test. - Run
ruff format .,ruff check .,mypyandpytest; all four must pass. - Open a pull request against
devand describe what changed and why.
main is protected: it only receives changes from dev through a pull
request, and releases are tags on main. Bugs and ideas can be reported as
issues.
Authors
- Andreas Auer (andreas.auer03@gmail.com)
- Sarah Goetz (sarahgoetz0603@gmail.com)
- Philipp Fuchs (philipp.fuchs@stud.plus.ac.at)
The Haralick feature extraction (src/pad_train/features/haralick.py) is based
on code by Chumakov, Demir and Waclawek.
Acknowledgements
The datasets used by this package are provided by Andreas Uhl (Department of Artificial Intelligence and Human Interfaces, University of Salzburg): the bona fide and presentation attack samples of PLUS, IDIAP and SCUT as well as the diffusion-based synthetic samples. The evaluation protocol follows the tasks he set in his courses on image processing and multimedia security. Thank you for making the data and the topic available.
Metadata
Release files for pad-train 1.0.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pad_train-1.0.1.tar.gz | 23.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pad_train-1.0.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 45.1 kB
Release files / pad_train-1.0.1.tar.gz
| Download URL | pad_train-1.0.1.tar.gz |
|---|---|
| Size | 23.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
66adef09f6c00b70fe3d41815944a82d676f7d92e0118edf356193c6caa1d437
|
|
BLAKE2b-256 checksum How to use checksums |
9f7636ef9e6eb2fe059b51ee530326d8a4ecfb8c1ab84e65c28ff2096be68d13
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.
Transparency logRelease files / pad_train-1.0.1-py3-none-any.whl
| Download URL | pad_train-1.0.1-py3-none-any.whl |
|---|---|
| Size | 21.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9020106cbaa4ab6acb90437371c82587af540ac1c5c4cad7f18dc45806ff60b6
|
|
BLAKE2b-256 checksum How to use checksums |
f70bfe8cea67d1a0a393fbeb145dacbadb063388a7829c37f93549f0ea62de7c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.
Transparency log