From 6aae4c3eb07cc2c33b68d7f273fc0329f2d3c30b Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Sat, 19 Sep 2026 13:52:52 +0200 Subject: [PATCH] Ship the three eye-state models beside the face pair MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- apps/darkroom-android/src/lib.rs | 14 +++-- docker/windows/README.md | 2 +- docs/windows.md | 5 +- models/face/2d106det_b1.onnx | 3 ++ models/face/README.md | 52 +++++++++++++++++++ models/face/ocec_s_b1.onnx | 3 ++ models/face/sgc_l_48_b1.onnx | 3 ++ packaging/PKGBUILD | 8 ++- packaging/flatpak/paris.tourolle.darkroom.yml | 3 +- packaging/windows/darkroom.nsi | 2 +- tools/fix-face-model-shapes.sh | 8 +++ 11 files changed, 94 insertions(+), 9 deletions(-) create mode 100644 models/face/2d106det_b1.onnx create mode 100644 models/face/ocec_s_b1.onnx create mode 100644 models/face/sgc_l_48_b1.onnx diff --git a/apps/darkroom-android/src/lib.rs b/apps/darkroom-android/src/lib.rs index 42031cc..dd62545 100644 --- a/apps/darkroom-android/src/lib.rs +++ b/apps/darkroom-android/src/lib.rs @@ -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 // obtain the one it was not shipped with. Twenty megabytes of APK for // 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_2.5g_640.onnx", "scrfd_2.5g_640.onnx"), (c"models/scrfd_10g_640.onnx", "scrfd_10g_640.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.classes.json", @@ -343,8 +351,8 @@ fn unpack_bundled_models(app: &slint::android::AndroidApp) { for (asset_path, name) in BUNDLED { let dest = dir.join(name); - // Already unpacked. Not re-read on every launch: this is 61 MB of - // copying across the seven entries, and the file does not change without + // Already unpacked. Not re-read on every launch: this is 73 MB of + // copying across the ten entries, and the file does not change without // 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 // costs nothing at all, which is what makes the second launch after an diff --git a/docker/windows/README.md b/docker/windows/README.md index c5f93d0..e3bc17f 100644 --- a/docker/windows/README.md +++ b/docker/windows/README.md @@ -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++` | | `wine darkroom-desktop.exe --version` | `darkroom-desktop 0.12.0`, exit 0, 0.1 s | | `makensis` | 105 MB `DarkRoom--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 | | 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. | diff --git a/docs/windows.md b/docs/windows.md index e0105b3..649af83 100644 --- a/docs/windows.md +++ b/docs/windows.md @@ -281,13 +281,14 @@ $LOCALAPPDATA\Programs\DarkRoom\ darkroom.exe models\ 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 LICENSE uninstall.exe ``` 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 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 @@ -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 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. | -| 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. | **What the first draft got wrong**, kept in place above with a note rather than rewritten, because diff --git a/models/face/2d106det_b1.onnx b/models/face/2d106det_b1.onnx new file mode 100644 index 0000000..67fb1cf --- /dev/null +++ b/models/face/2d106det_b1.onnx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:afc2984c285578412fee0a36b00b06374f99f22c39d9983583a5dcb03368ef26 +size 5030884 diff --git a/models/face/README.md b/models/face/README.md index 14b284b..8e1712d 100644 --- a/models/face/README.md +++ b/models/face/README.md @@ -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_10g_640.onnx 17 MB "Thorough" — a further 12% for 3× the time 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 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 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`. @@ -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_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 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 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. + +## 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). diff --git a/models/face/ocec_s_b1.onnx b/models/face/ocec_s_b1.onnx new file mode 100644 index 0000000..efbba03 --- /dev/null +++ b/models/face/ocec_s_b1.onnx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c848d34ce3989c7127cbf9506509f94be7fd4aa7b5bcd5e8a3d8ae4b29d04c7c +size 494519 diff --git a/models/face/sgc_l_48_b1.onnx b/models/face/sgc_l_48_b1.onnx new file mode 100644 index 0000000..f65c361 --- /dev/null +++ b/models/face/sgc_l_48_b1.onnx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d49b6206fb979e36550263776e0919f09a616ae27a93655897ace7e9538525c6 +size 6371507 diff --git a/packaging/PKGBUILD b/packaging/PKGBUILD index a315f4b..6c755f4 100644 --- a/packaging/PKGBUILD +++ b/packaging/PKGBUILD @@ -61,7 +61,13 @@ package() { # These live in LFS; a checkout without `git lfs pull` has ~130-byte # pointers here. Installing one produces a package whose face indexing # 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}" if [[ "$(stat -c%s "${_src}")" -lt 100000 ]]; then echo "error: ${_m} is an LFS pointer, not a model — run: git lfs pull" >&2 diff --git a/packaging/flatpak/paris.tourolle.darkroom.yml b/packaging/flatpak/paris.tourolle.darkroom.yml index 7d3aae9..e94d9b2 100644 --- a/packaging/flatpak/paris.tourolle.darkroom.yml +++ b/packaging/flatpak/paris.tourolle.darkroom.yml @@ -157,7 +157,8 @@ modules: # inside the graph loader on the user's machine. Refuse instead, with the # 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 echo "error: $m is an LFS pointer, not a model — run: git lfs pull" >&2 exit 1 diff --git a/packaging/windows/darkroom.nsi b/packaging/windows/darkroom.nsi index a5c0fab..f6dcbb3 100644 --- a/packaging/windows/darkroom.nsi +++ b/packaging/windows/darkroom.nsi @@ -70,7 +70,7 @@ Section "DarkRoom" SecMain File "${STAGE}\darkroom.exe" 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 — ; last, after the user's own directories, exactly as /usr/share is on Linux. SetOutPath "$INSTDIR\models" diff --git a/tools/fix-face-model-shapes.sh b/tools/fix-face-model-shapes.sh index 654f54b..9348fdc 100755 --- a/tools/fix-face-model-shapes.sh +++ b/tools/fix-face-model-shapes.sh @@ -11,6 +11,14 @@ # ... det_10g.onnx scrfd_10g_640.onnx --input input.1=1,3,640,640 # ... 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 # # The InsightFace exports declare dynamic input dimensions — SCRFD's H and W,