Ship the three eye-state models beside the face pair

2d106det for the eye contours, OCEC for open or closed, SGC for
sunglasses — all three pinned to a batch of one by the same script as
the pair, and installed by every packager so the eyes-open filter works
out of the box. The two classifiers are MIT, code and weights; the
README records their provenance, SGC's undocumented training set, and
the hashes as fetched and as shipped.
This commit is contained in:
2026-09-19 14:04:08 +02:00
parent 54b543fb77
commit 6aae4c3eb0
11 changed files with 94 additions and 9 deletions
+11 -3
View File
@@ -324,11 +324,19 @@ fn unpack_bundled_models(app: &slint::android::AndroidApp) {
// (`FaceDetector`, docs/faces.md §12.3) and a tablet has no other way to // (`FaceDetector`, docs/faces.md §12.3) and a tablet has no other way to
// obtain the one it was not shipped with. Twenty megabytes of APK for // obtain the one it was not shipped with. Twenty megabytes of APK for
// the choice; the embedder is the same for all three. // the choice; the embedder is the same for all three.
const BUNDLED: [(&std::ffi::CStr, &str); 7] = [ //
// Then the three eye-state models (docs/faces.md §17): landmarks, open
// or closed, sunglasses. The app indexes without them; with them the
// eyes-open filter has something to read, and a tablet has no other way
// to get them either.
const BUNDLED: [(&std::ffi::CStr, &str); 10] = [
(c"models/scrfd_500m_640.onnx", "scrfd_500m_640.onnx"), (c"models/scrfd_500m_640.onnx", "scrfd_500m_640.onnx"),
(c"models/scrfd_2.5g_640.onnx", "scrfd_2.5g_640.onnx"), (c"models/scrfd_2.5g_640.onnx", "scrfd_2.5g_640.onnx"),
(c"models/scrfd_10g_640.onnx", "scrfd_10g_640.onnx"), (c"models/scrfd_10g_640.onnx", "scrfd_10g_640.onnx"),
(c"models/arcface_mbf_b1.onnx", "arcface_mbf_b1.onnx"), (c"models/arcface_mbf_b1.onnx", "arcface_mbf_b1.onnx"),
(c"models/2d106det_b1.onnx", "2d106det_b1.onnx"),
(c"models/ocec_s_b1.onnx", "ocec_s_b1.onnx"),
(c"models/sgc_l_48_b1.onnx", "sgc_l_48_b1.onnx"),
(c"models/yolo26s-sem-ade20k.onnx", "yolo26s-sem-ade20k.onnx"), (c"models/yolo26s-sem-ade20k.onnx", "yolo26s-sem-ade20k.onnx"),
( (
c"models/yolo26s-sem-ade20k.classes.json", c"models/yolo26s-sem-ade20k.classes.json",
@@ -343,8 +351,8 @@ fn unpack_bundled_models(app: &slint::android::AndroidApp) {
for (asset_path, name) in BUNDLED { for (asset_path, name) in BUNDLED {
let dest = dir.join(name); let dest = dir.join(name);
// Already unpacked. Not re-read on every launch: this is 61 MB of // Already unpacked. Not re-read on every launch: this is 73 MB of
// copying across the seven entries, and the file does not change without // copying across the ten entries, and the file does not change without
// the APK changing, at which point the install wiped it anyway. It // the APK changing, at which point the install wiped it anyway. It
// matters more now than it did — a launch that skips every entry here // matters more now than it did — a launch that skips every entry here
// costs nothing at all, which is what makes the second launch after an // costs nothing at all, which is what makes the second launch after an
+1 -1
View File
@@ -41,7 +41,7 @@ Measured on the first build, 2026-09-12:
| DLL imports | 26 Windows system DLLs; **no** `libwinpthread-1.dll`, `libgcc_s`, `libstdc++` | | DLL imports | 26 Windows system DLLs; **no** `libwinpthread-1.dll`, `libgcc_s`, `libstdc++` |
| `wine darkroom-desktop.exe --version` | `darkroom-desktop 0.12.0`, exit 0, 0.1 s | | `wine darkroom-desktop.exe --version` | `darkroom-desktop 0.12.0`, exit 0, 0.1 s |
| `makensis` | 105 MB `DarkRoom-<version>-x86_64-setup.exe`, 64-bit stub | | `makensis` | 105 MB `DarkRoom-<version>-x86_64-setup.exe`, 64-bit stub |
| `wine setup.exe /S` | Installs exe + 7 models to `AppData\Local\Programs\DarkRoom`, writes the `HKCU` uninstall key; the installed exe runs | | `wine setup.exe /S` | Installs exe + 10 models to `AppData\Local\Programs\DarkRoom`, writes the `HKCU` uninstall key; the installed exe runs |
| `wine uninstall.exe /S` | Removes the directory and the key | | `wine uninstall.exe /S` | Removes the directory and the key |
| Start Menu shortcut | **Not verifiable here.** `CreateShortcut` is `IShellLink` and does nothing under a headless Wine; the directory beside it is created. Check on Windows. | | Start Menu shortcut | **Not verifiable here.** `CreateShortcut` is `IShellLink` and does nothing under a headless Wine; the directory beside it is created. Check on Windows. |
+3 -2
View File
@@ -281,13 +281,14 @@ $LOCALAPPDATA\Programs\DarkRoom\
darkroom.exe darkroom.exe
models\ models\
scrfd_500m_640.onnx scrfd_2.5g_640.onnx scrfd_10g_640.onnx arcface_mbf_b1.onnx scrfd_500m_640.onnx scrfd_2.5g_640.onnx scrfd_10g_640.onnx arcface_mbf_b1.onnx
2d106det_b1.onnx ocec_s_b1.onnx sgc_l_48_b1.onnx
yolo26s-sem-ade20k.onnx yolo26s-sem-ade20k.classes.json categories.txt yolo26s-sem-ade20k.onnx yolo26s-sem-ade20k.classes.json categories.txt
LICENSE LICENSE
uninstall.exe uninstall.exe
``` ```
Plus a Start Menu shortcut, and nothing on the desktop unless the user ticks it. The models are Plus a Start Menu shortcut, and nothing on the desktop unless the user ticks it. The models are
the same seven files the APK bundles and the PKGBUILD installs; `models\` beside the executable is the same ten files the APK bundles and the PKGBUILD installs; `models\` beside the executable is
where §3.2's lookup finds them. **No `LICENSE` yet**: the repository has no licence file at its where §3.2's lookup finds them. **No `LICENSE` yet**: the repository has no licence file at its
root (the Arch package points at the system's shared GPL text), so the installer has no licence root (the Arch package points at the system's shared GPL text), so the installer has no licence
page until one is added — a one-file change, and the `.nsi` says where the page then goes. The face weights carry the research-only grant that page until one is added — a one-file change, and the `.nsi` says where the page then goes. The face weights carry the research-only grant that
@@ -458,7 +459,7 @@ container was the cheaper way to get a pinned MinGW and a Wine that could be thr
| It links | Yes, first attempt once the link flags were right. 115 MB, `PE32+ … (GUI)`. Two dead-code warnings, both `cfg`-shadowed constants, fixed. | | It links | Yes, first attempt once the link flags were right. 115 MB, `PE32+ … (GUI)`. Two dead-code warnings, both `cfg`-shadowed constants, fixed. |
| It is a Windows executable | 26 imports, all Windows system DLLs. No MinGW runtime. `.rsrc` carries `PRODUCTVERSION 0,12,0,0`, `ProductName DarkRoom`, the icon. | | It is a Windows executable | 26 imports, all Windows system DLLs. No MinGW runtime. `.rsrc` carries `PRODUCTVERSION 0,12,0,0`, `ProductName DarkRoom`, the icon. |
| It starts | `wine darkroom-desktop.exe --version` → `darkroom-desktop 0.12.0`, exit 0, 0.1 s. | | It starts | `wine darkroom-desktop.exe --version` → `darkroom-desktop 0.12.0`, exit 0, 0.1 s. |
| The installer runs | `makensis` → 105 MB. `/S` installs the exe and seven models to `AppData\Local\Programs\DarkRoom`, writes the `HKCU` uninstall key; the installed exe runs; `uninstall.exe /S` removes directory and key. Shortcut unverifiable (§6). | | The installer runs | `makensis` → 105 MB. `/S` installs the exe and ten models to `AppData\Local\Programs\DarkRoom`, writes the `HKCU` uninstall key; the installed exe runs; `uninstall.exe /S` removes directory and key. Shortcut unverifiable (§6). |
| It draws a window | Not attempted. | | It draws a window | Not attempted. |
**What the first draft got wrong**, kept in place above with a note rather than rewritten, because **What the first draft got wrong**, kept in place above with a note rather than rewritten, because
Binary file not shown.
+52
View File
@@ -15,10 +15,19 @@ by every platform:
scrfd_2.5g_640.onnx 3.3 MB "Balanced" — 14% more faces for 12% more time (faces.md §12.3) scrfd_2.5g_640.onnx 3.3 MB "Balanced" — 14% more faces for 12% more time (faces.md §12.3)
scrfd_10g_640.onnx 17 MB "Thorough" — a further 12% for 3× the time scrfd_10g_640.onnx 17 MB "Thorough" — a further 12% for 3× the time
arcface_mbf_b1.onnx 13 MB arcface_mbf_b1.onnx 13 MB
2d106det_b1.onnx 4.8 MB 106 landmarks, for the eye boxes (faces.md §17)
ocec_s_b1.onnx 483 KB eyes open or closed, per eye
sgc_l_48_b1.onnx 6.1 MB sunglasses or not, per head
Which detector runs is a per-device setting (`FaceSettings::detector`); all three are installed so Which detector runs is a per-device setting (`FaceSettings::detector`); all three are installed so
the choice exists on every platform. Each is its own `faces.model_id`, so changing it re-indexes. the choice exists on every platform. Each is its own `faces.model_id`, so changing it re-indexes.
The last three are optional to the app: `library::face_models` reports them beside the pair when
all three are there, and a library without them indexes faces and simply has no eye readings —
every reader treats "never read" as unknown, never as closed. They are found in the *same*
directory as the pair, so a hand-placed pair does not pick up a package's eye models from a
directory it otherwise outranks.
**A clone without git-lfs gets a ~130-byte pointer where each model should be.** Both packagers check **A clone without git-lfs gets a ~130-byte pointer where each model should be.** Both packagers check
for exactly that and refuse, rather than shipping the pointer and failing inside tract on the user's for exactly that and refuse, rather than shipping the pointer and failing inside tract on the user's
machine. Fix it with `git lfs pull`. machine. Fix it with `git lfs pull`.
@@ -31,7 +40,50 @@ cannot parse any of the graphs while they are dynamic:
./tools/fix-face-model-shapes.sh det_2.5g.onnx models/face/scrfd_2.5g_640.onnx --input input.1=1,3,640,640 ./tools/fix-face-model-shapes.sh det_2.5g.onnx models/face/scrfd_2.5g_640.onnx --input input.1=1,3,640,640
./tools/fix-face-model-shapes.sh det_10g.onnx models/face/scrfd_10g_640.onnx --input input.1=1,3,640,640 ./tools/fix-face-model-shapes.sh det_10g.onnx models/face/scrfd_10g_640.onnx --input input.1=1,3,640,640
./tools/fix-face-model-shapes.sh w600k_mbf.onnx models/face/arcface_mbf_b1.onnx --dim None=1 ./tools/fix-face-model-shapes.sh w600k_mbf.onnx models/face/arcface_mbf_b1.onnx --dim None=1
./tools/fix-face-model-shapes.sh 2d106det.onnx models/face/2d106det_b1.onnx --dim None=1
`2d106det_b1.onnx` is from the same `buffalo_l.zip` as `det_10g.onnx` and under the same grant: the
106-point landmark model whose lid contours the eye boxes are cut from (docs/faces.md §17.2).
sha256 as fetched `f001b856…a7109dbf`, as shipped `afc2984c…03368ef26`.
The weights carry a non-commercial research-only grant. They are here because this is a private The weights carry a non-commercial research-only grant. They are here because this is a private
repository and self-installed builds; they come back out before anything is published, and the repository and self-installed builds; they come back out before anything is published, and the
restriction binds whoever uses the app, not only the project. docs/faces.md §2.2a is the decision. restriction binds whoever uses the app, not only the project. docs/faces.md §2.2a is the decision.
## The two classifiers are a different matter
`ocec_s_b1.onnx` and `sgc_l_48_b1.onnx` are **MIT, code and weights**, from Katsuya Hyodo's
ultra-lightweight classifier series — the same author as the whole-body detector the reference
pipeline uses. They are not under the InsightFace grant and do not come out when the project
publishes. Read 2026-09-19:
| | OCEC — open/closed eyes | SGC — sunglasses |
|---|---|---|
| Source | `github.com/PINTO0309/OCEC`, release `onnx`, `ocec_s.onnx` | `github.com/PINTO0309/SGC`, release `onnx`, `sgc_is_l_48x48.onnx` |
| Licence | MIT (repository `LICENSE`; no separate grant on the weights) | MIT, likewise |
| Training data | *Open and Closed Eyes* (Młodawski 2024, HF, **ODC-By 1.0** — attribution only), crops cut by DEIMv2-Wholebody34 (Apache 2.0) | **Not stated.** The README names no dataset and carries no acknowledgement; `data/` holds a class-ratio plot and nothing else. |
| Input | one eye, 40×24, RGB, `x/255` | one head, 48×48, RGB, `x/255` |
| Output | `prob_open`, a sigmoid | `prob_sunglasses`, a sigmoid |
| sha256 as fetched | `9a346a08…c604ba9b64` | `9c13d937…a4063c73` |
| sha256 as shipped | `c848d34c…4b29d04c7c` | `d49b6206…7e9538525c6` |
Both shipped with a dynamic batch dimension that tract loads but the project pins anyway, with the
same script as the pair:
./tools/fix-face-model-shapes.sh ocec_s.onnx models/face/ocec_s_b1.onnx --dim batch=1
./tools/fix-face-model-shapes.sh sgc_is_l_48x48.onnx models/face/sgc_l_48_b1.onnx --dim batch=1
The one thing D13's discipline turns up here is SGC's undocumented training set. The weights'
grant is MIT and that is what binds a redistributor; but "trained on what" is the question this
project reads first, and for SGC it has no answer. Recorded so it is a known gap rather than an
assumption, and so that whoever finds a sunglasses classifier with a stated dataset knows what to
replace.
Attribution, as ODC-By asks for the eye dataset:
> Michał Młodawski, *Open and Closed Eyes Dataset*, July 2024,
> https://huggingface.co/datasets/MichalMlodawski/closed-open-eyes
The variant choice was measured, not taken from the F1 column: on family snapshots the S variant
of OCEC read more open eyes as open than M or L did, which overfit their own domain
(docs/faces.md §17).
Binary file not shown.
Binary file not shown.
+7 -1
View File
@@ -61,7 +61,13 @@ package() {
# These live in LFS; a checkout without `git lfs pull` has ~130-byte # These live in LFS; a checkout without `git lfs pull` has ~130-byte
# pointers here. Installing one produces a package whose face indexing # pointers here. Installing one produces a package whose face indexing
# fails inside the graph loader on the user's machine, so refuse instead. # fails inside the graph loader on the user's machine, so refuse instead.
for _m in scrfd_500m_640.onnx scrfd_2.5g_640.onnx scrfd_10g_640.onnx arcface_mbf_b1.onnx; do #
# The last three are the eye-state models (docs/faces.md §17): landmarks,
# open/closed, sunglasses. Optional to the app, which indexes without
# them, but shipped beside the pair so the eyes-open filter works out of
# the box.
for _m in scrfd_500m_640.onnx scrfd_2.5g_640.onnx scrfd_10g_640.onnx arcface_mbf_b1.onnx \
2d106det_b1.onnx ocec_s_b1.onnx sgc_l_48_b1.onnx; do
_src="models/face/${_m}" _src="models/face/${_m}"
if [[ "$(stat -c%s "${_src}")" -lt 100000 ]]; then if [[ "$(stat -c%s "${_src}")" -lt 100000 ]]; then
echo "error: ${_m} is an LFS pointer, not a model — run: git lfs pull" >&2 echo "error: ${_m} is an LFS pointer, not a model — run: git lfs pull" >&2
@@ -157,7 +157,8 @@ modules:
# inside the graph loader on the user's machine. Refuse instead, with the # inside the graph loader on the user's machine. Refuse instead, with the
# command that fixes it. # command that fixes it.
- | - |
for m in scrfd_500m_640.onnx scrfd_2.5g_640.onnx scrfd_10g_640.onnx arcface_mbf_b1.onnx; do for m in scrfd_500m_640.onnx scrfd_2.5g_640.onnx scrfd_10g_640.onnx arcface_mbf_b1.onnx \
2d106det_b1.onnx ocec_s_b1.onnx sgc_l_48_b1.onnx; do
if [ "$(stat -c%s "models/face/$m")" -lt 100000 ]; then if [ "$(stat -c%s "models/face/$m")" -lt 100000 ]; then
echo "error: $m is an LFS pointer, not a model — run: git lfs pull" >&2 echo "error: $m is an LFS pointer, not a model — run: git lfs pull" >&2
exit 1 exit 1
+1 -1
View File
@@ -70,7 +70,7 @@ Section "DarkRoom" SecMain
File "${STAGE}\darkroom.exe" File "${STAGE}\darkroom.exe"
File "${STAGE}\LICENSE" File "${STAGE}\LICENSE"
; The seven model files (four face, three scene), beside the executable, ; The ten model files (seven face, three scene), beside the executable,
; which is where `library::system_face_models_dirs` looks on Windows — ; which is where `library::system_face_models_dirs` looks on Windows —
; last, after the user's own directories, exactly as /usr/share is on Linux. ; last, after the user's own directories, exactly as /usr/share is on Linux.
SetOutPath "$INSTDIR\models" SetOutPath "$INSTDIR\models"
+8
View File
@@ -11,6 +11,14 @@
# ... det_10g.onnx scrfd_10g_640.onnx --input input.1=1,3,640,640 # ... det_10g.onnx scrfd_10g_640.onnx --input input.1=1,3,640,640
# ... w600k_mbf.onnx arcface_mbf_b1.onnx --dim None=1 # ... w600k_mbf.onnx arcface_mbf_b1.onnx --dim None=1
# #
# And the three eye-state models, verified 2026-09-19 (docs/faces.md §17).
# The landmark model's batch is the literal "None" like the embedder's; the
# two classifiers' is a *named* dim_param "batch":
#
# ... 2d106det.onnx 2d106det_b1.onnx --dim None=1
# ... ocec_s.onnx ocec_s_b1.onnx --dim batch=1
# ... sgc_is_l_48x48.onnx sgc_l_48_b1.onnx --dim batch=1
#
# ## Why this exists # ## Why this exists
# #
# The InsightFace exports declare dynamic input dimensions — SCRFD's H and W, # The InsightFace exports declare dynamic input dimensions — SCRFD's H and W,