Skip to content

Add HSEmotion facial emotion recognition sample (EfficientNet-B0 on CompiledModel GPU)#222

Open
john-rocky wants to merge 2 commits into
google-ai-edge:mainfrom
john-rocky:hsemotion-facial-emotion-sample
Open

Add HSEmotion facial emotion recognition sample (EfficientNet-B0 on CompiledModel GPU)#222
john-rocky wants to merge 2 commits into
google-ai-edge:mainfrom
john-rocky:hsemotion-facial-emotion-sample

Conversation

@john-rocky

Copy link
Copy Markdown
Contributor

What

New facial_emotion_recognition/hsemotion_kotlin_gpu sample: HSEmotion (EmotiEffLib, Apache-2.0) — an EfficientNet-B0 fine-tuned on AffectNet, recognizing the 8 emotions (anger, contempt, disgust, fear, happiness, neutral, sadness, surprise) fully on the LiteRT CompiledModel GPU.

  • Kotlin sample app (com.google.ai.edge.examples.hsemotion): pick a face photo → the app crops the face with the built-in android.media.FaceDetector → emotion distribution. ~2 ms/inference.
  • conversion/: build_hsemotion.py (litert-torch) + validate_hsemotion.py (CompiledModel Python API parity) + README.
  • Model on HF: litert-community/HSEmotion-B0-LiteRT (8 MB fp16, fetched at build time by download_model.gradle, not committed).

Two conversion hurdles

  1. Old-timm pickle. The released weights are a pickled model built with an old timm whose forward is broken under current timm (missing conv_s2d). The state dict is lifted into a fresh timm tf_efficientnet_b0 (num_classes=8, classifier.0.*classifier.*), which has a working forward — 358/360 tensors match by name+shape.
  2. fp16 SqueezeExcite mean → NaN. The SE block's global mean x.mean((2,3)) over the 112×112 stem map is a single fp16 reduction whose partial sum overflows 65504 → the GPU delegate emits an all-NaN output (it reduces in fp16 even for an fp32 graph; desktop fp16 CPU is exact). Replaced by a hierarchical mean — repeated avg_pool2d over equal-size tiling windows (≤ 49 elements each) — mathematically identical, fp16-safe.

Verification (Pixel 8a, LiteRT 2.1.5, fp16)

  • 342/342 nodes on the GPU delegate, 1 partition, ~2 ms/inference.
  • fp16 tflite vs fp32 PyTorch via the CompiledModel API: top-1 identical, logits corr 1.00000 (validate_hsemotion.py PASS).
  • Device fp16 top-1 == desktop fp32 (logits corr 0.99997). App build + on-device run confirmed (bundled smiling-face sample → Happiness).
  • :app:assembleDebug builds.

Companion LiteRT-Models entry: john-rocky/LiteRT-Models#57.

…ompiledModel GPU)

HSEmotion (av-savchenko / EmotiEffLib, Apache-2.0): an EfficientNet-B0 fine-tuned on AffectNet, recognizing 8 emotions fully on the LiteRT CompiledModel GPU. New facial_emotion_recognition/hsemotion_kotlin_gpu sample.

Two conversion hurdles: (1) the released weights are an old-timm pickle whose forward is broken under current timm -> lift the state dict into a fresh timm tf_efficientnet_b0. (2) the SqueezeExcite global mean over the 112x112 stem map overflows the fp16 accumulator on the delegate -> all-NaN output; replaced by a hierarchical avg_pool mean (fp16-safe). Pixel 8a: 342/342 nodes, 1 partition, ~2 ms; device fp16 top-1 == desktop fp32 (logits corr 0.99997). conversion/validate_hsemotion.py = CompiledModel-API parity. App uses android.media.FaceDetector to crop the face. Model: litert-community/HSEmotion-B0-LiteRT.
Rework the HSEmotion sample UI from a single Activity-with-Views layout into
the canonical Compose + MVVM shape used by the sibling samples. This is a
UI-layer-only change; the inference helper is untouched.

- MainActivity is now a thin ComponentActivity that hosts the Compose UI and
  wires the gallery picker.
- MainViewModel owns EmotionClassifier, loads it from assets, and confines
  model creation plus every classify call to a single-parallelism dispatcher
  (the helper reuses native I/O buffers). The face-classification timing math
  moves over verbatim from the old Activity.
- UiState is an immutable snapshot carrying the input face bitmap, the ranked
  emotion predictions, the inference time, and any error text.
- EmotionScreen renders the status header, image picker, the input face, and a
  Column of emotion-score rows. All user-visible strings live in strings.xml,
  including the score format string.
- Model loading stays in assets via EmotionClassifier (unchanged); load
  failures now surface as an inline errorMessage. The build-time
  download_model.gradle fetch is retained.
- Adopt the version-catalog build (AGP 8.9.1 / Kotlin 2.2.21 / compileSdk 36)
  matching the sibling samples. LiteRT stays pinned at 2.1.5.

EmotionClassifier.kt (the inference helper) is byte-identical.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant