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
// 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
+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++` |
| `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 |
| `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. |
+3 -2
View File
@@ -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
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_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).
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
# 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
@@ -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
+1 -1
View File
@@ -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"
+8
View File
@@ -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,