pad-train 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pad_train-1.0.0/PKG-INFO +301 -0
- pad_train-1.0.0/README.md +275 -0
- pad_train-1.0.0/pyproject.toml +74 -0
- pad_train-1.0.0/setup.cfg +4 -0
- pad_train-1.0.0/src/pad_train/__init__.py +51 -0
- pad_train-1.0.0/src/pad_train/cli.py +66 -0
- pad_train-1.0.0/src/pad_train/config.py +172 -0
- pad_train-1.0.0/src/pad_train/data.py +205 -0
- pad_train-1.0.0/src/pad_train/evaluation.py +221 -0
- pad_train-1.0.0/src/pad_train/features/__init__.py +28 -0
- pad_train-1.0.0/src/pad_train/features/fourier.py +123 -0
- pad_train-1.0.0/src/pad_train/features/haralick.py +29 -0
- pad_train-1.0.0/src/pad_train/features/registry.py +92 -0
- pad_train-1.0.0/src/pad_train/metrics.py +46 -0
- pad_train-1.0.0/src/pad_train/py.typed +0 -0
- pad_train-1.0.0/src/pad_train.egg-info/PKG-INFO +301 -0
- pad_train-1.0.0/src/pad_train.egg-info/SOURCES.txt +20 -0
- pad_train-1.0.0/src/pad_train.egg-info/dependency_links.txt +1 -0
- pad_train-1.0.0/src/pad_train.egg-info/entry_points.txt +2 -0
- pad_train-1.0.0/src/pad_train.egg-info/requires.txt +11 -0
- pad_train-1.0.0/src/pad_train.egg-info/top_level.txt +1 -0
- pad_train-1.0.0/tests/test_pad_train.py +111 -0
pad_train-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pad_train
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: k-NN presentation attack detection (PAD) for finger vein data, trained on real or synthetic attack samples.
|
|
5
|
+
Author: Philipp Fuchs
|
|
6
|
+
Author-email: Andreas Auer <andreas.auer03@gmail.com>, Sarah Goetz <sarahgoetz0603@gmail.com>
|
|
7
|
+
Project-URL: Repository, https://github.com/Kneidl18/PAD_train
|
|
8
|
+
Keywords: biometrics,finger vein,presentation attack detection,k-NN,synthetic data
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: Image Recognition
|
|
13
|
+
Classifier: Typing :: Typed
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
Requires-Dist: mahotas>=1.4.18
|
|
17
|
+
Requires-Dist: numpy>=1.26
|
|
18
|
+
Requires-Dist: pandas>=2.0
|
|
19
|
+
Requires-Dist: pillow>=10.0
|
|
20
|
+
Requires-Dist: scikit-learn>=1.4
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
23
|
+
Requires-Dist: pandas-stubs; extra == "dev"
|
|
24
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
25
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
26
|
+
|
|
27
|
+
# pad_train
|
|
28
|
+
|
|
29
|
+
k-NN presentation attack detection (PAD) for finger vein images. The package
|
|
30
|
+
answers one question: **can a PAD system that is trained on synthetic attack
|
|
31
|
+
samples reliably tell real attacks from bona fide samples?**
|
|
32
|
+
|
|
33
|
+
It returns APCER, BPCER and ACER as a `pandas.DataFrame`.
|
|
34
|
+
|
|
35
|
+
## Method
|
|
36
|
+
|
|
37
|
+
Three sample types are involved:
|
|
38
|
+
|
|
39
|
+
| Type | Meaning | Source |
|
|
40
|
+
|------|-------------------------------------------|------------------------------|
|
|
41
|
+
| (1) | bona fide ("real") | `Data_FV_Spoofing_WS2025_26` |
|
|
42
|
+
| (2) | real presentation attack ("spoof") | `Data_FV_Spoofing_WS2025_26` |
|
|
43
|
+
| (3) | synthetic presentation attack (diffusion) | `AE_Res-diff`, `VAE-diff` |
|
|
44
|
+
|
|
45
|
+
Each dataset (PLUS, IDIAP, SCUT) is evaluated with a five-fold cross-validation.
|
|
46
|
+
The folds partition the **subjects**, so no person is in training and test at
|
|
47
|
+
the same time. Only type (1) samples that have a type (2) counterpart are used
|
|
48
|
+
(balanced sets). Two protocols run on identical folds:
|
|
49
|
+
|
|
50
|
+
| `protocol` | Training (4/5 of the subjects) | Test (remaining 1/5) |
|
|
51
|
+
|-------------|--------------------------------|----------------------|
|
|
52
|
+
| `baseline` | (1) vs. (2) | (1) vs. (2) |
|
|
53
|
+
| `synthetic` | (1) vs. (3) | (1) vs. (2) |
|
|
54
|
+
|
|
55
|
+
The classifier is a k-nearest-neighbour classifier on top of an exchangeable
|
|
56
|
+
feature extractor. The reported metrics follow ISO/IEC 30107-3:
|
|
57
|
+
|
|
58
|
+
- **APCER** – share of attacks classified as bona fide
|
|
59
|
+
- **BPCER** – share of bona fide samples classified as attack
|
|
60
|
+
- **ACER** – mean of the two
|
|
61
|
+
|
|
62
|
+
Details worth knowing:
|
|
63
|
+
|
|
64
|
+
- The synthetic samples carry no subject identity, so they cannot be split by
|
|
65
|
+
subject. By default each fold draws as many synthetic samples as it has bona
|
|
66
|
+
fide training samples (`balance_synthetic=True`), which keeps the training set
|
|
67
|
+
the same size as in the baseline. Synthetic samples are never part of a test set.
|
|
68
|
+
- "Has a counterpart" is decided per file name for PLUS and IDIAP (real and
|
|
69
|
+
spoof captures share their names) and per subject for SCUT.
|
|
70
|
+
- The metrics are computed per fold and then averaged.
|
|
71
|
+
|
|
72
|
+
## Installation
|
|
73
|
+
|
|
74
|
+
From PyPI:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pip install pad-train
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Directly from the repository, over SSH or HTTPS. The repository is private, so either way
|
|
81
|
+
you need access to it:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# SSH (needs an SSH key registered with GitHub)
|
|
85
|
+
pip install "git+ssh://git@github.com/Kneidl18/PAD_train.git" # latest main
|
|
86
|
+
pip install "git+ssh://git@github.com/Kneidl18/PAD_train.git@1.0.0" # a release
|
|
87
|
+
|
|
88
|
+
# HTTPS (uses your git credential helper, or asks for user name and access token)
|
|
89
|
+
pip install "git+https://github.com/Kneidl18/PAD_train.git" # latest main
|
|
90
|
+
pip install "git+https://github.com/Kneidl18/PAD_train.git@1.0.0" # a release
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
For local development:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
git clone git@github.com:Kneidl18/PAD_train.git
|
|
97
|
+
cd PAD_train
|
|
98
|
+
conda env create -f environment.yml # installs the package in editable mode
|
|
99
|
+
conda activate pad_train
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Data
|
|
103
|
+
|
|
104
|
+
The three archives (about 3 GB in total) are downloaded on demand into the
|
|
105
|
+
data root and removed after extraction:
|
|
106
|
+
|
|
107
|
+
```text
|
|
108
|
+
<data root>/
|
|
109
|
+
├── Data_FV_Spoofing_WS2025_26/ # types (1) and (2)
|
|
110
|
+
├── AE_Res-diff/ # type (3), residual auto encoder
|
|
111
|
+
└── VAE-diff/ # type (3), variational auto encoder
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The data root defaults to `./data` (relative to the working directory). Change
|
|
115
|
+
it in Python, through the environment, or on the command line:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from pathlib import Path
|
|
119
|
+
import pad_train
|
|
120
|
+
|
|
121
|
+
pad_train.settings.data_root = Path("/mnt/datasets/finger_vein")
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
export PAD_TRAIN_DATA_ROOT=/mnt/datasets/finger_vein
|
|
126
|
+
pad-train --data-root /mnt/datasets/finger_vein download
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
All paths, URLs and dataset layouts live in
|
|
130
|
+
[`src/pad_train/config.py`](src/pad_train/config.py).
|
|
131
|
+
|
|
132
|
+
Fetching happens in one of two ways:
|
|
133
|
+
|
|
134
|
+
- **Explicitly:** `pad-train download` (or `pad_train.download_data()`).
|
|
135
|
+
- **Implicitly:** `load_data()` and `evaluate()` download whatever is missing
|
|
136
|
+
and simply use what is already there. Pass `download=False` to forbid it.
|
|
137
|
+
|
|
138
|
+
> `pip` cannot run code at install time for wheels, so there is no
|
|
139
|
+
> `pip install --download-datasets`. Run `pad-train download` after installing.
|
|
140
|
+
|
|
141
|
+
## Usage
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
import pad_train
|
|
145
|
+
|
|
146
|
+
results = pad_train.evaluate(extractors=["fourier", "haralick"])
|
|
147
|
+
print(results)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
```text
|
|
151
|
+
dataset extractor protocol train_attack APCER BPCER ACER
|
|
152
|
+
PLUS fourier baseline spoof ...
|
|
153
|
+
PLUS fourier synthetic AE_Res ...
|
|
154
|
+
PLUS fourier synthetic VAE ...
|
|
155
|
+
...
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`train_attack` names the attack samples used for training: `spoof` for the
|
|
159
|
+
baseline, otherwise the synthetic source. All rates are fractions in `[0, 1]`.
|
|
160
|
+
|
|
161
|
+
Useful arguments of `evaluate()`:
|
|
162
|
+
|
|
163
|
+
| Argument | Default | Meaning |
|
|
164
|
+
|---------------------|----------------|-------------------------------------------------|
|
|
165
|
+
| `datasets` | all | e.g. `["PLUS"]` |
|
|
166
|
+
| `extractors` | `("fourier",)` | registered names or callables |
|
|
167
|
+
| `synthetic` | all | e.g. `["VAE"]` |
|
|
168
|
+
| `protocols` | both | `"baseline"` and/or `"synthetic"` |
|
|
169
|
+
| `n_neighbors` | per dataset | `k` of the k-NN (PLUS 6, IDIAP 1, SCUT 1) |
|
|
170
|
+
| `balance_synthetic` | `True` | sub-sample the synthetic training samples |
|
|
171
|
+
| `per_fold` | `False` | one row per fold instead of the mean |
|
|
172
|
+
| `download` | `True` | fetch missing data |
|
|
173
|
+
|
|
174
|
+
Lower-level building blocks: `load_data()` returns the images of one dataset,
|
|
175
|
+
`evaluate_dataset()` evaluates images, `cross_validate()` evaluates precomputed
|
|
176
|
+
feature matrices.
|
|
177
|
+
|
|
178
|
+
The same from the command line:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
pad-train evaluate --datasets PLUS IDIAP --extractors fourier haralick --output results.csv
|
|
182
|
+
pad-train evaluate --help
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Feature extractors
|
|
186
|
+
|
|
187
|
+
A feature extractor is any callable that maps a grayscale image (2-D `uint8`
|
|
188
|
+
array) to a 1-D feature vector whose length does not depend on the image size.
|
|
189
|
+
|
|
190
|
+
| Name | Description |
|
|
191
|
+
|------------|--------------------------------------------------------------------------|
|
|
192
|
+
| `fourier` | log energy of the magnitude spectrum in radial bands, thirds per dataset |
|
|
193
|
+
| `haralick` | 13 Haralick texture statistics, averaged over the four directions |
|
|
194
|
+
|
|
195
|
+
### Fourier settings
|
|
196
|
+
|
|
197
|
+
`fourier` uses only the magnitude of the spectrum; the phase is discarded. The
|
|
198
|
+
30 bands are grouped into thirds (1 = low, 2 = middle, 3 = high frequencies),
|
|
199
|
+
and each dataset uses its own selection:
|
|
200
|
+
|
|
201
|
+
| Dataset | `fourier_thirds` | Bands |
|
|
202
|
+
|---------|------------------|-------|
|
|
203
|
+
| PLUS | `(1, 2, 3)` | 0–29 |
|
|
204
|
+
| IDIAP | `(1, 2)` | 0–19 |
|
|
205
|
+
| SCUT | `(1,)` | 0–9 |
|
|
206
|
+
|
|
207
|
+
`evaluate()` and the command line apply these automatically. To change them,
|
|
208
|
+
edit `fourier_thirds` in `_default_datasets()` in
|
|
209
|
+
[`src/pad_train/config.py`](src/pad_train/config.py), or override them at run time:
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
pad_train.settings.datasets["SCUT"].fourier_thirds = (1, 2)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Adding an extractor
|
|
216
|
+
|
|
217
|
+
Adding one takes a single decorator, in this package
|
|
218
|
+
(`src/pad_train/features/`) or in your own code:
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
import numpy as np
|
|
222
|
+
import pad_train
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@pad_train.register_extractor("mean_std")
|
|
226
|
+
def mean_std(image):
|
|
227
|
+
return np.array([image.mean(), image.std()])
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# a parametrised variant of a built-in one: the 10 lowest frequency bands
|
|
231
|
+
pad_train.register_extractor("fourier_low", pad_train.make_fourier_extractor(bands=range(10)))
|
|
232
|
+
|
|
233
|
+
pad_train.evaluate(extractors=["fourier", "fourier_low", "mean_std"])
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Additional synthetic sources
|
|
237
|
+
|
|
238
|
+
Any folder with the layout `<path>/<dataset>/spoof/samples/*.png` can be used
|
|
239
|
+
as a source of type (3) samples, for example post-processed versions of the
|
|
240
|
+
provided ones:
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
from pathlib import Path
|
|
244
|
+
from pad_train import DataSource, evaluate, settings
|
|
245
|
+
|
|
246
|
+
settings.synthetic_sources["VAE_cleaned"] = DataSource("VAE_cleaned", Path("/data/VAE_cleaned"))
|
|
247
|
+
evaluate(synthetic=["VAE", "VAE_cleaned"])
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
The dataset folder names are `PLUS_matched`, `IDIAP` and `SCUT`.
|
|
251
|
+
|
|
252
|
+
## Development
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
ruff format . # formatting
|
|
256
|
+
ruff check . # linting (annotations and docstrings included)
|
|
257
|
+
mypy # static type checking
|
|
258
|
+
pytest # tests; they generate their own toy data, no download needed
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Releasing
|
|
262
|
+
|
|
263
|
+
1. Set the new `version` in `pyproject.toml`, merge it to `main`.
|
|
264
|
+
2. Publish a GitHub release on `main` whose tag is exactly that version (e.g. `1.0.0`).
|
|
265
|
+
|
|
266
|
+
The [`Publish to PyPI`](.github/workflows/publish.yml) workflow then checks,
|
|
267
|
+
builds and uploads the package.
|
|
268
|
+
|
|
269
|
+
## Contributing
|
|
270
|
+
|
|
271
|
+
Contributions are welcome through pull requests on
|
|
272
|
+
[GitHub](https://github.com/Kneidl18/PAD_train):
|
|
273
|
+
|
|
274
|
+
1. Fork the repository, or create a branch if you have write access, and
|
|
275
|
+
branch off `dev`.
|
|
276
|
+
2. Make your change. New feature extractors go into `src/pad_train/features/`
|
|
277
|
+
and need a test.
|
|
278
|
+
3. Run `ruff format .`, `ruff check .`, `mypy` and `pytest`; all four must pass.
|
|
279
|
+
4. Open a pull request against `dev` and describe what changed and why.
|
|
280
|
+
|
|
281
|
+
`main` is protected: it only receives changes from `dev` through a pull
|
|
282
|
+
request, and releases are tags on `main`. Bugs and ideas can be reported as
|
|
283
|
+
[issues](https://github.com/Kneidl18/PAD_train/issues).
|
|
284
|
+
|
|
285
|
+
## Authors
|
|
286
|
+
|
|
287
|
+
- Andreas Auer (<andreas.auer03@gmail.com>)
|
|
288
|
+
- Sarah Goetz (<sarahgoetz0603@gmail.com>)
|
|
289
|
+
- Philipp Fuchs
|
|
290
|
+
|
|
291
|
+
The Haralick feature extraction (`src/pad_train/features/haralick.py`) is based
|
|
292
|
+
on code by Chumakov, Demir and Waclawek.
|
|
293
|
+
|
|
294
|
+
## Acknowledgements
|
|
295
|
+
|
|
296
|
+
The datasets used by this package are provided by **Andreas Uhl** (Department
|
|
297
|
+
of Artificial Intelligence and Human Interfaces, University of Salzburg): the
|
|
298
|
+
bona fide and presentation attack samples of PLUS, IDIAP and SCUT as well as
|
|
299
|
+
the diffusion-based synthetic samples. The evaluation protocol follows the
|
|
300
|
+
tasks he set in his courses on image processing and multimedia security. Thank
|
|
301
|
+
you for making the data and the topic available.
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# pad_train
|
|
2
|
+
|
|
3
|
+
k-NN presentation attack detection (PAD) for finger vein images. The package
|
|
4
|
+
answers one question: **can a PAD system that is trained on synthetic attack
|
|
5
|
+
samples reliably tell real attacks from bona fide samples?**
|
|
6
|
+
|
|
7
|
+
It returns APCER, BPCER and ACER as a `pandas.DataFrame`.
|
|
8
|
+
|
|
9
|
+
## Method
|
|
10
|
+
|
|
11
|
+
Three sample types are involved:
|
|
12
|
+
|
|
13
|
+
| Type | Meaning | Source |
|
|
14
|
+
|------|-------------------------------------------|------------------------------|
|
|
15
|
+
| (1) | bona fide ("real") | `Data_FV_Spoofing_WS2025_26` |
|
|
16
|
+
| (2) | real presentation attack ("spoof") | `Data_FV_Spoofing_WS2025_26` |
|
|
17
|
+
| (3) | synthetic presentation attack (diffusion) | `AE_Res-diff`, `VAE-diff` |
|
|
18
|
+
|
|
19
|
+
Each dataset (PLUS, IDIAP, SCUT) is evaluated with a five-fold cross-validation.
|
|
20
|
+
The folds partition the **subjects**, so no person is in training and test at
|
|
21
|
+
the same time. Only type (1) samples that have a type (2) counterpart are used
|
|
22
|
+
(balanced sets). Two protocols run on identical folds:
|
|
23
|
+
|
|
24
|
+
| `protocol` | Training (4/5 of the subjects) | Test (remaining 1/5) |
|
|
25
|
+
|-------------|--------------------------------|----------------------|
|
|
26
|
+
| `baseline` | (1) vs. (2) | (1) vs. (2) |
|
|
27
|
+
| `synthetic` | (1) vs. (3) | (1) vs. (2) |
|
|
28
|
+
|
|
29
|
+
The classifier is a k-nearest-neighbour classifier on top of an exchangeable
|
|
30
|
+
feature extractor. The reported metrics follow ISO/IEC 30107-3:
|
|
31
|
+
|
|
32
|
+
- **APCER** – share of attacks classified as bona fide
|
|
33
|
+
- **BPCER** – share of bona fide samples classified as attack
|
|
34
|
+
- **ACER** – mean of the two
|
|
35
|
+
|
|
36
|
+
Details worth knowing:
|
|
37
|
+
|
|
38
|
+
- The synthetic samples carry no subject identity, so they cannot be split by
|
|
39
|
+
subject. By default each fold draws as many synthetic samples as it has bona
|
|
40
|
+
fide training samples (`balance_synthetic=True`), which keeps the training set
|
|
41
|
+
the same size as in the baseline. Synthetic samples are never part of a test set.
|
|
42
|
+
- "Has a counterpart" is decided per file name for PLUS and IDIAP (real and
|
|
43
|
+
spoof captures share their names) and per subject for SCUT.
|
|
44
|
+
- The metrics are computed per fold and then averaged.
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
From PyPI:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install pad-train
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Directly from the repository, over SSH or HTTPS. The repository is private, so either way
|
|
55
|
+
you need access to it:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# SSH (needs an SSH key registered with GitHub)
|
|
59
|
+
pip install "git+ssh://git@github.com/Kneidl18/PAD_train.git" # latest main
|
|
60
|
+
pip install "git+ssh://git@github.com/Kneidl18/PAD_train.git@1.0.0" # a release
|
|
61
|
+
|
|
62
|
+
# HTTPS (uses your git credential helper, or asks for user name and access token)
|
|
63
|
+
pip install "git+https://github.com/Kneidl18/PAD_train.git" # latest main
|
|
64
|
+
pip install "git+https://github.com/Kneidl18/PAD_train.git@1.0.0" # a release
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
For local development:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
git clone git@github.com:Kneidl18/PAD_train.git
|
|
71
|
+
cd PAD_train
|
|
72
|
+
conda env create -f environment.yml # installs the package in editable mode
|
|
73
|
+
conda activate pad_train
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Data
|
|
77
|
+
|
|
78
|
+
The three archives (about 3 GB in total) are downloaded on demand into the
|
|
79
|
+
data root and removed after extraction:
|
|
80
|
+
|
|
81
|
+
```text
|
|
82
|
+
<data root>/
|
|
83
|
+
├── Data_FV_Spoofing_WS2025_26/ # types (1) and (2)
|
|
84
|
+
├── AE_Res-diff/ # type (3), residual auto encoder
|
|
85
|
+
└── VAE-diff/ # type (3), variational auto encoder
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The data root defaults to `./data` (relative to the working directory). Change
|
|
89
|
+
it in Python, through the environment, or on the command line:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from pathlib import Path
|
|
93
|
+
import pad_train
|
|
94
|
+
|
|
95
|
+
pad_train.settings.data_root = Path("/mnt/datasets/finger_vein")
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
export PAD_TRAIN_DATA_ROOT=/mnt/datasets/finger_vein
|
|
100
|
+
pad-train --data-root /mnt/datasets/finger_vein download
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
All paths, URLs and dataset layouts live in
|
|
104
|
+
[`src/pad_train/config.py`](src/pad_train/config.py).
|
|
105
|
+
|
|
106
|
+
Fetching happens in one of two ways:
|
|
107
|
+
|
|
108
|
+
- **Explicitly:** `pad-train download` (or `pad_train.download_data()`).
|
|
109
|
+
- **Implicitly:** `load_data()` and `evaluate()` download whatever is missing
|
|
110
|
+
and simply use what is already there. Pass `download=False` to forbid it.
|
|
111
|
+
|
|
112
|
+
> `pip` cannot run code at install time for wheels, so there is no
|
|
113
|
+
> `pip install --download-datasets`. Run `pad-train download` after installing.
|
|
114
|
+
|
|
115
|
+
## Usage
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
import pad_train
|
|
119
|
+
|
|
120
|
+
results = pad_train.evaluate(extractors=["fourier", "haralick"])
|
|
121
|
+
print(results)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
```text
|
|
125
|
+
dataset extractor protocol train_attack APCER BPCER ACER
|
|
126
|
+
PLUS fourier baseline spoof ...
|
|
127
|
+
PLUS fourier synthetic AE_Res ...
|
|
128
|
+
PLUS fourier synthetic VAE ...
|
|
129
|
+
...
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`train_attack` names the attack samples used for training: `spoof` for the
|
|
133
|
+
baseline, otherwise the synthetic source. All rates are fractions in `[0, 1]`.
|
|
134
|
+
|
|
135
|
+
Useful arguments of `evaluate()`:
|
|
136
|
+
|
|
137
|
+
| Argument | Default | Meaning |
|
|
138
|
+
|---------------------|----------------|-------------------------------------------------|
|
|
139
|
+
| `datasets` | all | e.g. `["PLUS"]` |
|
|
140
|
+
| `extractors` | `("fourier",)` | registered names or callables |
|
|
141
|
+
| `synthetic` | all | e.g. `["VAE"]` |
|
|
142
|
+
| `protocols` | both | `"baseline"` and/or `"synthetic"` |
|
|
143
|
+
| `n_neighbors` | per dataset | `k` of the k-NN (PLUS 6, IDIAP 1, SCUT 1) |
|
|
144
|
+
| `balance_synthetic` | `True` | sub-sample the synthetic training samples |
|
|
145
|
+
| `per_fold` | `False` | one row per fold instead of the mean |
|
|
146
|
+
| `download` | `True` | fetch missing data |
|
|
147
|
+
|
|
148
|
+
Lower-level building blocks: `load_data()` returns the images of one dataset,
|
|
149
|
+
`evaluate_dataset()` evaluates images, `cross_validate()` evaluates precomputed
|
|
150
|
+
feature matrices.
|
|
151
|
+
|
|
152
|
+
The same from the command line:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
pad-train evaluate --datasets PLUS IDIAP --extractors fourier haralick --output results.csv
|
|
156
|
+
pad-train evaluate --help
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Feature extractors
|
|
160
|
+
|
|
161
|
+
A feature extractor is any callable that maps a grayscale image (2-D `uint8`
|
|
162
|
+
array) to a 1-D feature vector whose length does not depend on the image size.
|
|
163
|
+
|
|
164
|
+
| Name | Description |
|
|
165
|
+
|------------|--------------------------------------------------------------------------|
|
|
166
|
+
| `fourier` | log energy of the magnitude spectrum in radial bands, thirds per dataset |
|
|
167
|
+
| `haralick` | 13 Haralick texture statistics, averaged over the four directions |
|
|
168
|
+
|
|
169
|
+
### Fourier settings
|
|
170
|
+
|
|
171
|
+
`fourier` uses only the magnitude of the spectrum; the phase is discarded. The
|
|
172
|
+
30 bands are grouped into thirds (1 = low, 2 = middle, 3 = high frequencies),
|
|
173
|
+
and each dataset uses its own selection:
|
|
174
|
+
|
|
175
|
+
| Dataset | `fourier_thirds` | Bands |
|
|
176
|
+
|---------|------------------|-------|
|
|
177
|
+
| PLUS | `(1, 2, 3)` | 0–29 |
|
|
178
|
+
| IDIAP | `(1, 2)` | 0–19 |
|
|
179
|
+
| SCUT | `(1,)` | 0–9 |
|
|
180
|
+
|
|
181
|
+
`evaluate()` and the command line apply these automatically. To change them,
|
|
182
|
+
edit `fourier_thirds` in `_default_datasets()` in
|
|
183
|
+
[`src/pad_train/config.py`](src/pad_train/config.py), or override them at run time:
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
pad_train.settings.datasets["SCUT"].fourier_thirds = (1, 2)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Adding an extractor
|
|
190
|
+
|
|
191
|
+
Adding one takes a single decorator, in this package
|
|
192
|
+
(`src/pad_train/features/`) or in your own code:
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
import numpy as np
|
|
196
|
+
import pad_train
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@pad_train.register_extractor("mean_std")
|
|
200
|
+
def mean_std(image):
|
|
201
|
+
return np.array([image.mean(), image.std()])
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# a parametrised variant of a built-in one: the 10 lowest frequency bands
|
|
205
|
+
pad_train.register_extractor("fourier_low", pad_train.make_fourier_extractor(bands=range(10)))
|
|
206
|
+
|
|
207
|
+
pad_train.evaluate(extractors=["fourier", "fourier_low", "mean_std"])
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Additional synthetic sources
|
|
211
|
+
|
|
212
|
+
Any folder with the layout `<path>/<dataset>/spoof/samples/*.png` can be used
|
|
213
|
+
as a source of type (3) samples, for example post-processed versions of the
|
|
214
|
+
provided ones:
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
from pathlib import Path
|
|
218
|
+
from pad_train import DataSource, evaluate, settings
|
|
219
|
+
|
|
220
|
+
settings.synthetic_sources["VAE_cleaned"] = DataSource("VAE_cleaned", Path("/data/VAE_cleaned"))
|
|
221
|
+
evaluate(synthetic=["VAE", "VAE_cleaned"])
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
The dataset folder names are `PLUS_matched`, `IDIAP` and `SCUT`.
|
|
225
|
+
|
|
226
|
+
## Development
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
ruff format . # formatting
|
|
230
|
+
ruff check . # linting (annotations and docstrings included)
|
|
231
|
+
mypy # static type checking
|
|
232
|
+
pytest # tests; they generate their own toy data, no download needed
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Releasing
|
|
236
|
+
|
|
237
|
+
1. Set the new `version` in `pyproject.toml`, merge it to `main`.
|
|
238
|
+
2. Publish a GitHub release on `main` whose tag is exactly that version (e.g. `1.0.0`).
|
|
239
|
+
|
|
240
|
+
The [`Publish to PyPI`](.github/workflows/publish.yml) workflow then checks,
|
|
241
|
+
builds and uploads the package.
|
|
242
|
+
|
|
243
|
+
## Contributing
|
|
244
|
+
|
|
245
|
+
Contributions are welcome through pull requests on
|
|
246
|
+
[GitHub](https://github.com/Kneidl18/PAD_train):
|
|
247
|
+
|
|
248
|
+
1. Fork the repository, or create a branch if you have write access, and
|
|
249
|
+
branch off `dev`.
|
|
250
|
+
2. Make your change. New feature extractors go into `src/pad_train/features/`
|
|
251
|
+
and need a test.
|
|
252
|
+
3. Run `ruff format .`, `ruff check .`, `mypy` and `pytest`; all four must pass.
|
|
253
|
+
4. Open a pull request against `dev` and describe what changed and why.
|
|
254
|
+
|
|
255
|
+
`main` is protected: it only receives changes from `dev` through a pull
|
|
256
|
+
request, and releases are tags on `main`. Bugs and ideas can be reported as
|
|
257
|
+
[issues](https://github.com/Kneidl18/PAD_train/issues).
|
|
258
|
+
|
|
259
|
+
## Authors
|
|
260
|
+
|
|
261
|
+
- Andreas Auer (<andreas.auer03@gmail.com>)
|
|
262
|
+
- Sarah Goetz (<sarahgoetz0603@gmail.com>)
|
|
263
|
+
- Philipp Fuchs
|
|
264
|
+
|
|
265
|
+
The Haralick feature extraction (`src/pad_train/features/haralick.py`) is based
|
|
266
|
+
on code by Chumakov, Demir and Waclawek.
|
|
267
|
+
|
|
268
|
+
## Acknowledgements
|
|
269
|
+
|
|
270
|
+
The datasets used by this package are provided by **Andreas Uhl** (Department
|
|
271
|
+
of Artificial Intelligence and Human Interfaces, University of Salzburg): the
|
|
272
|
+
bona fide and presentation attack samples of PLUS, IDIAP and SCUT as well as
|
|
273
|
+
the diffusion-based synthetic samples. The evaluation protocol follows the
|
|
274
|
+
tasks he set in his courses on image processing and multimedia security. Thank
|
|
275
|
+
you for making the data and the topic available.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pad_train"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "k-NN presentation attack detection (PAD) for finger vein data, trained on real or synthetic attack samples."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Andreas Auer", email = "andreas.auer03@gmail.com" },
|
|
13
|
+
{ name = "Sarah Goetz", email = "sarahgoetz0603@gmail.com"},
|
|
14
|
+
{ name = "Philipp Fuchs"},
|
|
15
|
+
]
|
|
16
|
+
keywords = ["biometrics", "finger vein", "presentation attack detection", "k-NN", "synthetic data"]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 5 - Production/Stable",
|
|
19
|
+
"Intended Audience :: Science/Research",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Topic :: Scientific/Engineering :: Image Recognition",
|
|
22
|
+
"Typing :: Typed",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"mahotas>=1.4.18",
|
|
26
|
+
"numpy>=1.26",
|
|
27
|
+
"pandas>=2.0",
|
|
28
|
+
"pillow>=10.0",
|
|
29
|
+
"scikit-learn>=1.4",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
dev = [
|
|
34
|
+
"mypy>=1.10",
|
|
35
|
+
"pandas-stubs",
|
|
36
|
+
"pytest>=8.0",
|
|
37
|
+
"ruff>=0.6",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[project.scripts]
|
|
41
|
+
pad-train = "pad_train.cli:main"
|
|
42
|
+
|
|
43
|
+
[project.urls]
|
|
44
|
+
Repository = "https://github.com/Kneidl18/PAD_train"
|
|
45
|
+
|
|
46
|
+
[tool.setuptools.packages.find]
|
|
47
|
+
where = ["src"]
|
|
48
|
+
|
|
49
|
+
[tool.setuptools.package-data]
|
|
50
|
+
pad_train = ["py.typed"]
|
|
51
|
+
|
|
52
|
+
[tool.ruff]
|
|
53
|
+
line-length = 100
|
|
54
|
+
src = ["src", "tests"]
|
|
55
|
+
|
|
56
|
+
[tool.ruff.lint]
|
|
57
|
+
select = ["E", "F", "W", "I", "N", "UP", "B", "ANN", "D", "SIM", "PTH", "RUF"]
|
|
58
|
+
|
|
59
|
+
[tool.ruff.lint.pydocstyle]
|
|
60
|
+
convention = "google"
|
|
61
|
+
|
|
62
|
+
[tool.ruff.lint.per-file-ignores]
|
|
63
|
+
"tests/*" = ["D103"]
|
|
64
|
+
|
|
65
|
+
[tool.mypy]
|
|
66
|
+
strict = true
|
|
67
|
+
files = ["src"]
|
|
68
|
+
|
|
69
|
+
[[tool.mypy.overrides]]
|
|
70
|
+
module = ["mahotas.*", "sklearn.*"]
|
|
71
|
+
ignore_missing_imports = true
|
|
72
|
+
|
|
73
|
+
[tool.pytest.ini_options]
|
|
74
|
+
testpaths = ["tests"]
|