ofiqpy 0.1.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.
Files changed (38) hide show
  1. ofiqpy-0.1.0/LICENSE +21 -0
  2. ofiqpy-0.1.0/NOTICE +27 -0
  3. ofiqpy-0.1.0/PKG-INFO +178 -0
  4. ofiqpy-0.1.0/README.md +143 -0
  5. ofiqpy-0.1.0/ofiqpy/__init__.py +61 -0
  6. ofiqpy-0.1.0/ofiqpy/align.py +109 -0
  7. ofiqpy-0.1.0/ofiqpy/batch.py +102 -0
  8. ofiqpy-0.1.0/ofiqpy/cli.py +47 -0
  9. ofiqpy-0.1.0/ofiqpy/config.py +101 -0
  10. ofiqpy-0.1.0/ofiqpy/detectors/__init__.py +0 -0
  11. ofiqpy-0.1.0/ofiqpy/detectors/ssd.py +48 -0
  12. ofiqpy-0.1.0/ofiqpy/landmarks/__init__.py +0 -0
  13. ofiqpy-0.1.0/ofiqpy/landmarks/adnet.py +84 -0
  14. ofiqpy-0.1.0/ofiqpy/measures/__init__.py +0 -0
  15. ofiqpy-0.1.0/ofiqpy/measures/core.py +174 -0
  16. ofiqpy-0.1.0/ofiqpy/measures/geometry.py +90 -0
  17. ofiqpy-0.1.0/ofiqpy/measures/helpers.py +32 -0
  18. ofiqpy-0.1.0/ofiqpy/measures/models.py +126 -0
  19. ofiqpy-0.1.0/ofiqpy/measures/pixel.py +178 -0
  20. ofiqpy-0.1.0/ofiqpy/native_cv.py +63 -0
  21. ofiqpy-0.1.0/ofiqpy/output.py +42 -0
  22. ofiqpy-0.1.0/ofiqpy/pipeline.py +69 -0
  23. ofiqpy-0.1.0/ofiqpy/pose/__init__.py +0 -0
  24. ofiqpy-0.1.0/ofiqpy/pose/tddfa.py +68 -0
  25. ofiqpy-0.1.0/ofiqpy/segmentation/__init__.py +0 -0
  26. ofiqpy-0.1.0/ofiqpy/segmentation/occlusion.py +28 -0
  27. ofiqpy-0.1.0/ofiqpy/segmentation/parsing.py +31 -0
  28. ofiqpy-0.1.0/ofiqpy/session.py +45 -0
  29. ofiqpy-0.1.0/ofiqpy/sigmoid.py +42 -0
  30. ofiqpy-0.1.0/ofiqpy.egg-info/PKG-INFO +178 -0
  31. ofiqpy-0.1.0/ofiqpy.egg-info/SOURCES.txt +36 -0
  32. ofiqpy-0.1.0/ofiqpy.egg-info/dependency_links.txt +1 -0
  33. ofiqpy-0.1.0/ofiqpy.egg-info/entry_points.txt +2 -0
  34. ofiqpy-0.1.0/ofiqpy.egg-info/requires.txt +16 -0
  35. ofiqpy-0.1.0/ofiqpy.egg-info/top_level.txt +1 -0
  36. ofiqpy-0.1.0/pyproject.toml +60 -0
  37. ofiqpy-0.1.0/setup.cfg +4 -0
  38. ofiqpy-0.1.0/tests/test_smoke.py +57 -0
ofiqpy-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aaron Storey
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
ofiqpy-0.1.0/NOTICE ADDED
@@ -0,0 +1,27 @@
1
+ ofiqpy — a faithful Python port of OFIQ v1.1.0
2
+ ==============================================
3
+
4
+ ofiqpy reproduces the algorithms of OFIQ (Open Source Face Image Quality), the
5
+ ISO/IEC 29794-5 reference implementation developed by the German Federal Office
6
+ for Information Security (BSI), Copyright (c) 2024, MIT-licensed:
7
+
8
+ https://github.com/BSI-OFIQ/OFIQ-Project
9
+
10
+ Models are NOT bundled
11
+ ----------------------
12
+ ofiqpy loads OFIQ's own model files at runtime (SSD face detector, ADNet-98
13
+ landmarks, 3DDFA-V2 pose, BiSeNet face parsing, face-occlusion segmentation,
14
+ sharpness random forest, compression CNN, HSEmotion + AdaBoost expression, and
15
+ the MagFace unified-quality model). These files ship with OFIQ and may be
16
+ licensed separately from the OFIQ source (see OFIQ's LICENSE.md). They are NOT
17
+ redistributed with ofiqpy.
18
+
19
+ To run ofiqpy, obtain the models from an OFIQ install and set:
20
+
21
+ export OFIQPY_OFIQ_DATA=/path/to/OFIQ-Project/data
22
+
23
+ Conformance
24
+ -----------
25
+ ofiqpy targets agreement with the OFIQ reference to within +/- 1 quality point
26
+ per component (the ISO/IEC 29794-5 Annex A criterion). See the docs for the
27
+ validated conformance figures.
ofiqpy-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,178 @@
1
+ Metadata-Version: 2.4
2
+ Name: ofiqpy
3
+ Version: 0.1.0
4
+ Summary: A faithful Python port of OFIQ v1.1.0 (ISO/IEC 29794-5 face image quality), matching the reference within ±1.
5
+ Author: Aaron Storey
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/AVHBAC/ofiqpy
8
+ Project-URL: Documentation, https://avhbac.github.io/ofiqpy/
9
+ Project-URL: Repository, https://github.com/AVHBAC/ofiqpy
10
+ Project-URL: Issues, https://github.com/AVHBAC/ofiqpy/issues
11
+ Keywords: face,image quality,biometrics,ISO 29794-5,OFIQ,FIQA
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Scientific/Engineering :: Image Recognition
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ License-File: NOTICE
21
+ Requires-Dist: numpy<2,>=1.23
22
+ Requires-Dist: opencv-python-headless==4.5.5.64
23
+ Requires-Dist: onnxruntime==1.18.1
24
+ Provides-Extra: verify
25
+ Requires-Dist: pandas>=1.5; extra == "verify"
26
+ Provides-Extra: docs
27
+ Requires-Dist: mkdocs-material>=9.5; extra == "docs"
28
+ Requires-Dist: mkdocstrings[python]>=0.24; extra == "docs"
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest>=7; extra == "dev"
31
+ Requires-Dist: ruff>=0.4; extra == "dev"
32
+ Requires-Dist: pandas>=1.5; extra == "dev"
33
+ Requires-Dist: jupyter>=1.0; extra == "dev"
34
+ Dynamic: license-file
35
+
36
+ # ofiqpy — a faithful Python port of OFIQ v1.1.0
37
+
38
+ [![CI & Release](https://github.com/AVHBAC/ofiqpy/actions/workflows/workflow.yml/badge.svg)](https://github.com/AVHBAC/ofiqpy/actions/workflows/workflow.yml)
39
+ [![Docs](https://github.com/AVHBAC/ofiqpy/actions/workflows/docs.yml/badge.svg)](https://avhbac.github.io/ofiqpy/)
40
+ [![PyPI](https://img.shields.io/pypi/v/ofiqpy.svg)](https://pypi.org/project/ofiqpy/)
41
+ [![Python](https://img.shields.io/pypi/pyversions/ofiqpy.svg)](https://pypi.org/project/ofiqpy/)
42
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
43
+
44
+ A behavior-preserving Python reimplementation of the BSI **OFIQ** ISO/IEC 29794-5
45
+ face image quality library. `ofiqpy` **reuses OFIQ's own model files and reproduces
46
+ its exact `.cpp` algorithms**, matching the live OFIQ reference to within **±1** per
47
+ component (the ISO/IEC 29794-5 Annex A.2 conformance criterion).
48
+
49
+ 📖 **Docs: <https://avhbac.github.io/ofiqpy/>**
50
+
51
+ > Status: **full coverage + near-exact parity.** All 27 ISO components + UnifiedQualityScore,
52
+ > an OFIQ-compatible CSV writer, a CLI, and a parallel batch runner — all gated against
53
+ > live `OFIQSampleApp`. Validated on **1,000+ real CelebA images**: 27/28 components fully
54
+ > conformant (±1 on every image, most bit-exact); ~99.99% of all component-image pairs
55
+ > within ±1. The rare per-image residuals are numerical boundaries of discrete/learned
56
+ > models (RTrees vote, AdaBoost score, round tie), not algorithm gaps.
57
+
58
+ ## Install & run
59
+
60
+ ```bash
61
+ pip install ofiqpy
62
+ export OFIQPY_OFIQ_DATA=/path/to/OFIQ-Project/data # OFIQ's models (not bundled)
63
+
64
+ ofiqpy -i face.jpg -o out.csv # CLI
65
+ ```
66
+ ```python
67
+ from ofiqpy import assess
68
+ scores = assess("face.jpg") # {component: (raw, scalar)}
69
+ print(scores["UnifiedQualityScore"]) # (magnitude, 0-100)
70
+ ```
71
+
72
+ See [Installation](https://avhbac.github.io/ofiqpy/installation/) and
73
+ [Quickstart](https://avhbac.github.io/ofiqpy/quickstart/).
74
+
75
+ ## Design
76
+
77
+ - **Same weights.** Models are loaded directly from the reference checkout
78
+ `OFIQ-Project/data/models/` via `config.py` (SSD caffemodel, ADNet-98 ONNX,
79
+ 3DDFA-V2 ONNX, BiSeNet parsing ONNX, occlusion-seg ONNX, ssim-248 ONNX, MagFace ONNX).
80
+ No re-training, no substitute backbones.
81
+ - **Same math.** Detection, ADNet landmark crop/denorm, the 5-point LMEDS similarity
82
+ alignment to 616×616, the landmarked-region mask, `tmetric`, the generic
83
+ `h·(a+s·sigmoid(x;x0,w))` scalar mapping, and each measure are ported line-faithfully
84
+ from OFIQ's C++ (see per-module docstrings for `file:line` provenance).
85
+ - **Same versions.** The isolated `.venv` pins **OpenCV 4.5.5** and **onnxruntime 1.18.1**
86
+ to match OFIQ's build (`libofiq_lib.so` links OpenCV 4.5.5; bundles onnxruntime 1.18.1).
87
+ - **Gated, not asserted.** `tests/verify_ofiq.py` runs live OFIQ and checks
88
+ `|port_scalar − ofiq_scalar| ≤ 1` per image; `tests/gate_slice.py` reports it.
89
+
90
+ ## Conformance (1,000+ real CelebA images, port vs live OFIQ, ISO Annex A ±1)
91
+
92
+ Validated on **1,197 real CelebA images**: **27 of 28 components fully conformant** (±1 on
93
+ every image), 24 of them **bit-exact** (maxΔ=0). **~99.99% of all component-image pairs are
94
+ within ISO ±1** (mean |Δ| ≤ 0.02 for every component).
95
+
96
+ The rare per-image residuals:
97
+ - **Sharpness — 1 image at Δ=2**: OFIQ's RTrees vote count differs by exactly 2 trees at a
98
+ split-threshold knife-edge (a sub-LSB feature difference flips 2 borderline votes through
99
+ the step-function forest). Bit-exact on the rest.
100
+ - **BackgroundUniformity / ExpressionNeutrality / NoHeadCoverings — one ±1 image each**, a
101
+ single sigmoid/round boundary.
102
+
103
+ Sharpness (RTrees) and ExpressionNeutrality (dual EfficientNet + AdaBoost) run OFIQ's own
104
+ `cv2.ml` / ONNX models; UnifiedQualityScore runs OFIQ's MagFace ONNX. These four residuals
105
+ are numerical boundaries of discrete/learned models, not algorithm gaps.
106
+
107
+ ### How parity was reached (the residual was a bug, not a build limit)
108
+
109
+ An earlier version of this port was only ~96% conformant, and the residual was *wrongly*
110
+ attributed to a build-level OpenCV float difference. To test that, ctypes bridges were
111
+ built against OFIQ's own conan OpenCV static libs (`native/ofiq_cv.cpp`, `ofiq_ssd.cpp`)
112
+ and used to compare OFIQ's compiled `estimateAffinePartial2D`, `warpAffine`, `resize`, and
113
+ the SSD dnn forward pass against the pip `opencv-python` wheel. **Every OpenCV operation
114
+ was bit-identical** — which disproved the build-level theory and localized the divergence
115
+ to the **ADNet landmark back-projection**: OFIQ scales landmarks back with
116
+ `squareBox.height / 256` (`adnet_landmarks.cpp:313`), but `makeSquareBoundingBox`'s
117
+ `floor`/`ceil` can leave the box 1px non-square, and the port had used the *width*. On
118
+ exactly-square detector boxes it matched; otherwise it drifted ~1px, propagating (via the
119
+ alignment source points → affine → whole aligned face) into every landmark-sensitive
120
+ measure. One-character fix (width→height); all 27 non-model components went bit-exact.
121
+
122
+ The bridges in `native/` are diagnostic only — the runtime uses pip `cv2`, which is
123
+ bit-identical to OFIQ's OpenCV. No source build of OpenCV was needed.
124
+
125
+ ## Layout
126
+
127
+ ```
128
+ ofiqpy/
129
+ config.py JAXN loader + OFIQ model resolver + sigmoid params
130
+ sigmoid.py OFIQ ScalarConversion (Measure.h:271-285)
131
+ session.py shared preprocessing products
132
+ pipeline.py detect -> pose -> landmarks -> align -> parse -> occlusion -> region
133
+ detectors/ssd.py SSD (OpenCV DNN, Caffe)
134
+ landmarks/adnet.py ADNet-98 + square-crop helpers
135
+ align.py 616x616 alignment, landmarked region (GetFaceMask), tmetric, luminance
136
+ pose/tddfa.py 3DDFA-V2 pose
137
+ segmentation/ BiSeNet parsing, face-occlusion seg
138
+ measures/
139
+ core.py model cache + dispatch; C03/C09/C17/C20, unified, HeadPose (slot swap)
140
+ geometry.py C11,C12,C13,C19,C24-C27
141
+ pixel.py C01,C02,C04(var),C05,C06,C07,C10
142
+ models.py C08 Sharpness (RTrees), C14/C15/C16 occlusion, C18 Expression
143
+ helpers.py get_distance/get_middle, c_round, landmark index maps
144
+ output.py OFIQ-format CSV (named cols, raw + .scalar)
145
+ cli.py OFIQSampleApp-compatible CLI
146
+ tests/
147
+ verify_ofiq.py runs live OFIQ, ±1 gate
148
+ gate_slice.py full-coverage conformance runner
149
+ ```
150
+
151
+ ## Run
152
+
153
+ ```bash
154
+ export OFIQPY_OFIQ_DATA=/path/to/OFIQ-Project/data
155
+
156
+ ofiqpy -i <image|dir> -o out.csv # single / small runs (OFIQ-format CSV)
157
+ python -m ofiqpy.batch -i <dir> -o out.csv -w 8 --resume # parallel batch
158
+
159
+ # reproduce the conformance gate (needs a built OFIQSampleApp)
160
+ export OFIQPY_OFIQ_ROOT=/path/to/OFIQ-Project
161
+ export OFIQPY_TEST_IMAGES=/path/to/images
162
+ python tests/gate_slice.py 1000
163
+ ```
164
+
165
+ Full documentation: <https://avhbac.github.io/ofiqpy/>.
166
+
167
+ ## License & attribution
168
+
169
+ `ofiqpy` is released under the [MIT License](LICENSE).
170
+
171
+ It is a faithful port of **OFIQ** (Open Source Face Image Quality), developed by the German
172
+ Federal Office for Information Security (BSI), Copyright © 2024, MIT-licensed
173
+ (<https://github.com/BSI-OFIQ/OFIQ-Project>). Please acknowledge OFIQ when using ofiqpy.
174
+
175
+ **Models are not bundled.** ofiqpy loads OFIQ's own model files at runtime; they ship with
176
+ OFIQ and may be licensed separately (see OFIQ's `LICENSE.md`). Obtain them from an OFIQ
177
+ install and set `OFIQPY_OFIQ_DATA`. See [`NOTICE`](NOTICE) and the
178
+ [licensing docs](https://avhbac.github.io/ofiqpy/licensing/).
ofiqpy-0.1.0/README.md ADDED
@@ -0,0 +1,143 @@
1
+ # ofiqpy — a faithful Python port of OFIQ v1.1.0
2
+
3
+ [![CI & Release](https://github.com/AVHBAC/ofiqpy/actions/workflows/workflow.yml/badge.svg)](https://github.com/AVHBAC/ofiqpy/actions/workflows/workflow.yml)
4
+ [![Docs](https://github.com/AVHBAC/ofiqpy/actions/workflows/docs.yml/badge.svg)](https://avhbac.github.io/ofiqpy/)
5
+ [![PyPI](https://img.shields.io/pypi/v/ofiqpy.svg)](https://pypi.org/project/ofiqpy/)
6
+ [![Python](https://img.shields.io/pypi/pyversions/ofiqpy.svg)](https://pypi.org/project/ofiqpy/)
7
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
8
+
9
+ A behavior-preserving Python reimplementation of the BSI **OFIQ** ISO/IEC 29794-5
10
+ face image quality library. `ofiqpy` **reuses OFIQ's own model files and reproduces
11
+ its exact `.cpp` algorithms**, matching the live OFIQ reference to within **±1** per
12
+ component (the ISO/IEC 29794-5 Annex A.2 conformance criterion).
13
+
14
+ 📖 **Docs: <https://avhbac.github.io/ofiqpy/>**
15
+
16
+ > Status: **full coverage + near-exact parity.** All 27 ISO components + UnifiedQualityScore,
17
+ > an OFIQ-compatible CSV writer, a CLI, and a parallel batch runner — all gated against
18
+ > live `OFIQSampleApp`. Validated on **1,000+ real CelebA images**: 27/28 components fully
19
+ > conformant (±1 on every image, most bit-exact); ~99.99% of all component-image pairs
20
+ > within ±1. The rare per-image residuals are numerical boundaries of discrete/learned
21
+ > models (RTrees vote, AdaBoost score, round tie), not algorithm gaps.
22
+
23
+ ## Install & run
24
+
25
+ ```bash
26
+ pip install ofiqpy
27
+ export OFIQPY_OFIQ_DATA=/path/to/OFIQ-Project/data # OFIQ's models (not bundled)
28
+
29
+ ofiqpy -i face.jpg -o out.csv # CLI
30
+ ```
31
+ ```python
32
+ from ofiqpy import assess
33
+ scores = assess("face.jpg") # {component: (raw, scalar)}
34
+ print(scores["UnifiedQualityScore"]) # (magnitude, 0-100)
35
+ ```
36
+
37
+ See [Installation](https://avhbac.github.io/ofiqpy/installation/) and
38
+ [Quickstart](https://avhbac.github.io/ofiqpy/quickstart/).
39
+
40
+ ## Design
41
+
42
+ - **Same weights.** Models are loaded directly from the reference checkout
43
+ `OFIQ-Project/data/models/` via `config.py` (SSD caffemodel, ADNet-98 ONNX,
44
+ 3DDFA-V2 ONNX, BiSeNet parsing ONNX, occlusion-seg ONNX, ssim-248 ONNX, MagFace ONNX).
45
+ No re-training, no substitute backbones.
46
+ - **Same math.** Detection, ADNet landmark crop/denorm, the 5-point LMEDS similarity
47
+ alignment to 616×616, the landmarked-region mask, `tmetric`, the generic
48
+ `h·(a+s·sigmoid(x;x0,w))` scalar mapping, and each measure are ported line-faithfully
49
+ from OFIQ's C++ (see per-module docstrings for `file:line` provenance).
50
+ - **Same versions.** The isolated `.venv` pins **OpenCV 4.5.5** and **onnxruntime 1.18.1**
51
+ to match OFIQ's build (`libofiq_lib.so` links OpenCV 4.5.5; bundles onnxruntime 1.18.1).
52
+ - **Gated, not asserted.** `tests/verify_ofiq.py` runs live OFIQ and checks
53
+ `|port_scalar − ofiq_scalar| ≤ 1` per image; `tests/gate_slice.py` reports it.
54
+
55
+ ## Conformance (1,000+ real CelebA images, port vs live OFIQ, ISO Annex A ±1)
56
+
57
+ Validated on **1,197 real CelebA images**: **27 of 28 components fully conformant** (±1 on
58
+ every image), 24 of them **bit-exact** (maxΔ=0). **~99.99% of all component-image pairs are
59
+ within ISO ±1** (mean |Δ| ≤ 0.02 for every component).
60
+
61
+ The rare per-image residuals:
62
+ - **Sharpness — 1 image at Δ=2**: OFIQ's RTrees vote count differs by exactly 2 trees at a
63
+ split-threshold knife-edge (a sub-LSB feature difference flips 2 borderline votes through
64
+ the step-function forest). Bit-exact on the rest.
65
+ - **BackgroundUniformity / ExpressionNeutrality / NoHeadCoverings — one ±1 image each**, a
66
+ single sigmoid/round boundary.
67
+
68
+ Sharpness (RTrees) and ExpressionNeutrality (dual EfficientNet + AdaBoost) run OFIQ's own
69
+ `cv2.ml` / ONNX models; UnifiedQualityScore runs OFIQ's MagFace ONNX. These four residuals
70
+ are numerical boundaries of discrete/learned models, not algorithm gaps.
71
+
72
+ ### How parity was reached (the residual was a bug, not a build limit)
73
+
74
+ An earlier version of this port was only ~96% conformant, and the residual was *wrongly*
75
+ attributed to a build-level OpenCV float difference. To test that, ctypes bridges were
76
+ built against OFIQ's own conan OpenCV static libs (`native/ofiq_cv.cpp`, `ofiq_ssd.cpp`)
77
+ and used to compare OFIQ's compiled `estimateAffinePartial2D`, `warpAffine`, `resize`, and
78
+ the SSD dnn forward pass against the pip `opencv-python` wheel. **Every OpenCV operation
79
+ was bit-identical** — which disproved the build-level theory and localized the divergence
80
+ to the **ADNet landmark back-projection**: OFIQ scales landmarks back with
81
+ `squareBox.height / 256` (`adnet_landmarks.cpp:313`), but `makeSquareBoundingBox`'s
82
+ `floor`/`ceil` can leave the box 1px non-square, and the port had used the *width*. On
83
+ exactly-square detector boxes it matched; otherwise it drifted ~1px, propagating (via the
84
+ alignment source points → affine → whole aligned face) into every landmark-sensitive
85
+ measure. One-character fix (width→height); all 27 non-model components went bit-exact.
86
+
87
+ The bridges in `native/` are diagnostic only — the runtime uses pip `cv2`, which is
88
+ bit-identical to OFIQ's OpenCV. No source build of OpenCV was needed.
89
+
90
+ ## Layout
91
+
92
+ ```
93
+ ofiqpy/
94
+ config.py JAXN loader + OFIQ model resolver + sigmoid params
95
+ sigmoid.py OFIQ ScalarConversion (Measure.h:271-285)
96
+ session.py shared preprocessing products
97
+ pipeline.py detect -> pose -> landmarks -> align -> parse -> occlusion -> region
98
+ detectors/ssd.py SSD (OpenCV DNN, Caffe)
99
+ landmarks/adnet.py ADNet-98 + square-crop helpers
100
+ align.py 616x616 alignment, landmarked region (GetFaceMask), tmetric, luminance
101
+ pose/tddfa.py 3DDFA-V2 pose
102
+ segmentation/ BiSeNet parsing, face-occlusion seg
103
+ measures/
104
+ core.py model cache + dispatch; C03/C09/C17/C20, unified, HeadPose (slot swap)
105
+ geometry.py C11,C12,C13,C19,C24-C27
106
+ pixel.py C01,C02,C04(var),C05,C06,C07,C10
107
+ models.py C08 Sharpness (RTrees), C14/C15/C16 occlusion, C18 Expression
108
+ helpers.py get_distance/get_middle, c_round, landmark index maps
109
+ output.py OFIQ-format CSV (named cols, raw + .scalar)
110
+ cli.py OFIQSampleApp-compatible CLI
111
+ tests/
112
+ verify_ofiq.py runs live OFIQ, ±1 gate
113
+ gate_slice.py full-coverage conformance runner
114
+ ```
115
+
116
+ ## Run
117
+
118
+ ```bash
119
+ export OFIQPY_OFIQ_DATA=/path/to/OFIQ-Project/data
120
+
121
+ ofiqpy -i <image|dir> -o out.csv # single / small runs (OFIQ-format CSV)
122
+ python -m ofiqpy.batch -i <dir> -o out.csv -w 8 --resume # parallel batch
123
+
124
+ # reproduce the conformance gate (needs a built OFIQSampleApp)
125
+ export OFIQPY_OFIQ_ROOT=/path/to/OFIQ-Project
126
+ export OFIQPY_TEST_IMAGES=/path/to/images
127
+ python tests/gate_slice.py 1000
128
+ ```
129
+
130
+ Full documentation: <https://avhbac.github.io/ofiqpy/>.
131
+
132
+ ## License & attribution
133
+
134
+ `ofiqpy` is released under the [MIT License](LICENSE).
135
+
136
+ It is a faithful port of **OFIQ** (Open Source Face Image Quality), developed by the German
137
+ Federal Office for Information Security (BSI), Copyright © 2024, MIT-licensed
138
+ (<https://github.com/BSI-OFIQ/OFIQ-Project>). Please acknowledge OFIQ when using ofiqpy.
139
+
140
+ **Models are not bundled.** ofiqpy loads OFIQ's own model files at runtime; they ship with
141
+ OFIQ and may be licensed separately (see OFIQ's `LICENSE.md`). Obtain them from an OFIQ
142
+ install and set `OFIQPY_OFIQ_DATA`. See [`NOTICE`](NOTICE) and the
143
+ [licensing docs](https://avhbac.github.io/ofiqpy/licensing/).
@@ -0,0 +1,61 @@
1
+ """ofiqpy — a faithful Python port of OFIQ v1.1.0 (ISO/IEC 29794-5 face image quality).
2
+
3
+ Reuses OFIQ's own models and reproduces its exact algorithms, matching the reference
4
+ within +/- 1 quality point per component. Point ofiqpy at an OFIQ install's data/ dir:
5
+
6
+ export OFIQPY_OFIQ_DATA=/path/to/OFIQ-Project/data
7
+
8
+ Then:
9
+
10
+ from ofiqpy import assess
11
+ scores = assess("face.jpg") # {component_name: (raw, scalar)}
12
+ """
13
+ from __future__ import annotations
14
+
15
+ __version__ = "0.1.0"
16
+
17
+ _PIPE = None
18
+ _MEAS = None
19
+
20
+
21
+ def _lazy():
22
+ """Build (once) the shared pipeline + measures. Models load on first use."""
23
+ global _PIPE, _MEAS
24
+ if _PIPE is None:
25
+ from .config import OFIQConfig
26
+ from .measures.core import Measures
27
+ from .pipeline import OFIQPipeline
28
+ cfg = OFIQConfig()
29
+ _PIPE = OFIQPipeline(cfg)
30
+ _MEAS = Measures(cfg)
31
+ return _PIPE, _MEAS
32
+
33
+
34
+ def assess(image: "str | object") -> dict:
35
+ """Assess one image and return ``{component_name: (raw, scalar)}``.
36
+
37
+ Args:
38
+ image: path to an image file (str or PathLike), or a BGR uint8 numpy
39
+ array of shape (H, W, 3).
40
+
41
+ Returns:
42
+ Mapping of OFIQ component name to ``(raw native value, 0-100 scalar)``. Empty
43
+ if no face is detected.
44
+ """
45
+ import numpy as np
46
+
47
+ pipe, meas = _lazy()
48
+ if isinstance(image, np.ndarray):
49
+ bgr = image
50
+ else:
51
+ import cv2
52
+ bgr = cv2.imread(str(image))
53
+ if bgr is None:
54
+ raise FileNotFoundError(f"could not read image: {image}")
55
+ session = pipe.process(bgr)
56
+ if session.bbox is None:
57
+ return {}
58
+ return meas.compute(session)
59
+
60
+
61
+ __all__ = ["assess", "__version__"]
@@ -0,0 +1,109 @@
1
+ """Alignment (616x616), landmarked-region mask, tmetric, luminance.
2
+
3
+ Faithful port of utils.cpp:236-331, FaceMeasures.cpp:98-228, image_utils.cpp:43-112.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import math
8
+
9
+ import cv2
10
+ import numpy as np
11
+
12
+ # ADNet FaceMap indices (adnet_FaceMap.h)
13
+ LEFT_EYE_CORNERS = (60, 64)
14
+ RIGHT_EYE_CORNERS = (68, 72)
15
+ NOSE = 54
16
+ RIGHT_MOUTH = 76
17
+ LEFT_MOUTH = 82
18
+ CHIN = 16
19
+
20
+ REF_POINTS = np.float32([
21
+ [251, 272], [364, 272], [308, 336], [262, 402], [355, 402],
22
+ ])
23
+
24
+
25
+ def _round(x):
26
+ return math.copysign(math.floor(abs(x) + 0.5), x)
27
+
28
+
29
+ def eye_centers(P: np.ndarray):
30
+ """Midpoints of eye-corner pairs, each coord rounded (utils.cpp:302-317)."""
31
+ l = (_round(P[LEFT_EYE_CORNERS[0], 0] + 0.5 * (P[LEFT_EYE_CORNERS[1], 0] - P[LEFT_EYE_CORNERS[0], 0])),
32
+ _round(P[LEFT_EYE_CORNERS[0], 1] + 0.5 * (P[LEFT_EYE_CORNERS[1], 1] - P[LEFT_EYE_CORNERS[0], 1])))
33
+ r = (_round(P[RIGHT_EYE_CORNERS[0], 0] + 0.5 * (P[RIGHT_EYE_CORNERS[1], 0] - P[RIGHT_EYE_CORNERS[0], 0])),
34
+ _round(P[RIGHT_EYE_CORNERS[0], 1] + 0.5 * (P[RIGHT_EYE_CORNERS[1], 1] - P[RIGHT_EYE_CORNERS[0], 1])))
35
+ return l, r
36
+
37
+
38
+ def align(img: np.ndarray, P: np.ndarray):
39
+ """Return (aligned 616x616 BGR, aligned_landmarks (98,2), affine 2x3)."""
40
+ Lc, Rc = eye_centers(P)
41
+ src = np.float32([
42
+ [Lc[0], Lc[1]], [Rc[0], Rc[1]],
43
+ [P[NOSE, 0], P[NOSE, 1]],
44
+ [P[RIGHT_MOUTH, 0], P[RIGHT_MOUTH, 1]],
45
+ [P[LEFT_MOUTH, 0], P[LEFT_MOUTH, 1]],
46
+ ])
47
+ M, _ = cv2.estimateAffinePartial2D(src, REF_POINTS, method=cv2.LMEDS)
48
+ aligned = cv2.warpAffine(img, M, (616, 616)) # default INTER_LINEAR, BORDER_CONSTANT 0
49
+ ap = cv2.transform(P.reshape(-1, 1, 2).astype(np.float32), M).reshape(-1, 2)
50
+ aligned_lms = np.array([[_round(x), _round(y)] for x, y in ap], np.float64)
51
+ return aligned, aligned_lms, M
52
+
53
+
54
+ def tmetric(P: np.ndarray) -> float:
55
+ """||chin - eye_midpoint|| (utils.cpp:319-331). Space-agnostic; pass the right landmarks."""
56
+ Lc, Rc = eye_centers(P)
57
+ eye_mid = ((Lc[0] + Rc[0]) / 2.0, (Lc[1] + Rc[1]) / 2.0)
58
+ chin = P[CHIN]
59
+ return float(math.hypot(chin[0] - eye_mid[0], chin[1] - eye_mid[1]))
60
+
61
+
62
+ def landmarked_region(aligned_lms: np.ndarray, height=616, width=616, alpha=0.0) -> np.ndarray:
63
+ """GetFaceMask at alpha=0 (FaceMeasures.cpp:173-226): convex hull -> 224 -> resize."""
64
+ pts = aligned_lms.astype(np.int32)
65
+ hull = cv2.convexHull(pts)
66
+ rx, ry, rw, rh = cv2.boundingRect(hull)
67
+ b = int(ry - rh * 0.05)
68
+ d = int(ry + rh * 1.05)
69
+ a = int(rx + rw / 2.0 - (d - b) / 2.0)
70
+ c = int(rx + rw / 2.0 + (d - b) / 2.0)
71
+ S = 224
72
+ hull_s = ((hull.reshape(-1, 2).astype(np.float64) - (a, b)) / (d - b) * S).astype(np.int32)
73
+ mask = np.zeros((S, S), np.uint8)
74
+ cv2.fillConvexPoly(mask, hull_s, 1)
75
+ mrs = cv2.resize(mask, (c - a, d - b), interpolation=cv2.INTER_NEAREST)
76
+ region = np.zeros((height, width), np.uint8)
77
+ left, top = 0, 0
78
+ right, bottom = mrs.shape[1], mrs.shape[0]
79
+ an, bn, cn, dn = a, b, c, d
80
+ if a < 0:
81
+ left = -a
82
+ an = 0
83
+ if c > width:
84
+ right = mrs.shape[1] - (c - width)
85
+ cn = width
86
+ if b < 0:
87
+ top = -b
88
+ bn = 0
89
+ if d > height:
90
+ bottom = mrs.shape[0] - (d - height)
91
+ dn = height
92
+ region[bn:dn, an:cn] = mrs[top:bottom, left:right]
93
+ return region
94
+
95
+
96
+ # --- luminance (image_utils.cpp:43-112) ---
97
+ _SRGB_LUT = np.array([
98
+ ((v / 255.0) / 12.92) if (v / 255.0) <= 0.04045 else (((v / 255.0) + 0.055) / 1.055) ** 2.4
99
+ for v in range(256)
100
+ ], np.float64)
101
+
102
+
103
+ def luminance(img_bgr: np.ndarray) -> np.ndarray:
104
+ """sRGB-linearized Rec.709 luma -> floor(y*255+0.5) uint8 (BGR input)."""
105
+ B = _SRGB_LUT[img_bgr[:, :, 0]]
106
+ G = _SRGB_LUT[img_bgr[:, :, 1]]
107
+ R = _SRGB_LUT[img_bgr[:, :, 2]]
108
+ y = 0.2126 * R + 0.7152 * G + 0.0722 * B
109
+ return np.floor(y * 255.0 + 0.5).astype(np.uint8)
@@ -0,0 +1,102 @@
1
+ """Parallel batch runner — assess a directory of images to an OFIQ-format CSV.
2
+
3
+ Each worker process builds its own pipeline (ONNX/cv2.ml models are not shared
4
+ across processes). Resumable: rows already present in the output CSV are skipped.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import argparse
9
+ import csv
10
+ import time
11
+ from concurrent.futures import ProcessPoolExecutor
12
+ from pathlib import Path
13
+
14
+ import cv2
15
+
16
+ from .output import header, row
17
+
18
+ EXTS = {".jpg", ".jpeg", ".png", ".bmp"}
19
+
20
+ # per-worker singletons
21
+ _PIPE = None
22
+ _MEAS = None
23
+
24
+
25
+ def _init_worker():
26
+ global _PIPE, _MEAS
27
+ from .config import OFIQConfig
28
+ from .measures.core import Measures
29
+ from .pipeline import OFIQPipeline
30
+ cfg = OFIQConfig()
31
+ _PIPE = OFIQPipeline(cfg)
32
+ _MEAS = Measures(cfg)
33
+
34
+
35
+ def _assess_one(path_str: str) -> str:
36
+ t0 = time.time()
37
+ bgr = cv2.imread(path_str)
38
+ try:
39
+ s = _PIPE.process(bgr)
40
+ res = _MEAS.compute(s) if s.bbox is not None else {}
41
+ except Exception:
42
+ res = {}
43
+ return row(path_str, res, (time.time() - t0) * 1000)
44
+
45
+
46
+ def discover(input_path: Path) -> list[Path]:
47
+ p = Path(input_path)
48
+ if p.is_file():
49
+ return [p]
50
+ return sorted(f for f in p.rglob("*") if f.suffix.lower() in EXTS)
51
+
52
+
53
+ def _already_done(out_csv: Path) -> set[str]:
54
+ if not out_csv.exists():
55
+ return set()
56
+ done = set()
57
+ with open(out_csv, newline="") as f:
58
+ r = csv.reader(f, delimiter=";")
59
+ next(r, None) # header
60
+ for line in r:
61
+ if line:
62
+ done.add(line[0])
63
+ return done
64
+
65
+
66
+ def run_batch(input_path, output_csv, workers=None, resume=False, progress=True):
67
+ images = discover(Path(input_path))
68
+ out = Path(output_csv)
69
+ done = _already_done(out) if resume else set()
70
+ todo = [im for im in images if im.name not in done]
71
+
72
+ write_header = not (resume and out.exists())
73
+ mode = "a" if (resume and out.exists()) else "w"
74
+ n_done = 0
75
+ t0 = time.time()
76
+ with open(out, mode) as fh, ProcessPoolExecutor(max_workers=workers,
77
+ initializer=_init_worker) as ex:
78
+ if write_header:
79
+ fh.write(header() + "\n")
80
+ for line in ex.map(_assess_one, [str(im) for im in todo], chunksize=1):
81
+ fh.write(line + "\n")
82
+ fh.flush()
83
+ n_done += 1
84
+ if progress and n_done % 25 == 0:
85
+ rate = n_done / max(time.time() - t0, 1e-6)
86
+ print(f" {n_done}/{len(todo)} {rate:.1f} img/s", flush=True)
87
+ print(f"done: {n_done} images -> {out} ({len(done)} pre-existing skipped)")
88
+ return out
89
+
90
+
91
+ def main():
92
+ ap = argparse.ArgumentParser(description="ofiqpy parallel batch runner")
93
+ ap.add_argument("-i", "--input", required=True, help="image directory (recursive) or file")
94
+ ap.add_argument("-o", "--output", required=True, help="output CSV")
95
+ ap.add_argument("-w", "--workers", type=int, default=None, help="worker processes (default: CPUs)")
96
+ ap.add_argument("--resume", action="store_true", help="skip images already in the output CSV")
97
+ args = ap.parse_args()
98
+ run_batch(args.input, args.output, args.workers, args.resume)
99
+
100
+
101
+ if __name__ == "__main__":
102
+ main()