Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -159,13 +159,9 @@ class CameraManager(private val context: Context) {
analysis = analysis,
)
attachZoomObserver()
// The build marker suffix is the stale-APK tripwire (Lesson 012's environment
// notes): a logcat whose bind lines lack it was made by a build without the
// flick feature, whatever the checkout says.
Log.i(
TAG,
"Camera bound (lens=${if (lensFacing == CameraSelector.LENS_FACING_BACK) "back" else "front"}) " +
"build=hand-flick-v1"
"Camera bound (lens=${if (lensFacing == CameraSelector.LENS_FACING_BACK) "back" else "front"})"
)

onCameraReady()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,10 @@ import kotlin.math.hypot
*
* ## ID churn
*
* When ML Kit loses a face for a frame or two it often re-detects it under a **new** tracking id.
* The old id is still being held, so without care the same person would wear two lenses for up to
* [holdMs] — and, with the slots full, the new id would be locked out until the hold expired. So
* the nearest fresh face that has no slot and stands within [SAME_FACE_RADIUS_UNITS] of a
* held-but-unseen slot holder is taken to be that person. A different person cannot land on the
* exact same spot inside a third of a second, and a third person entering anywhere else during
* someone's blink still cannot steal a slot.
*
* The adopted face is **published under the original id**, not the detector's new one (Lesson
* 037). Every per-face consumer downstream — the wobble springs and eased mouth in [LensMotion],
* anything added later — keys its state on [FaceSnapshot.trackingId], so a label change that
* leaked through would reset all of it mid-blink. The new id is recorded as an alias and folded
* back onto the original on every later sighting, for as long as that person is held.
* ML Kit often re-detects a briefly lost face under a new tracking id. The roster adopts the
* nearest fresh face within [SAME_FACE_RADIUS_UNITS] of a held-but-unseen slot as the same person,
* publishing it under the original id (Lesson 037) so downstream per-face state keyed on
* [FaceSnapshot.trackingId] is not reset mid-blink.
*/
class FaceRoster(private val maxFaces: Int, private val holdMs: Long) {

Expand Down
20 changes: 1 addition & 19 deletions app/src/main/java/io/github/stozo04/openloop/camera/lens/Lens.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,27 +34,9 @@ import io.github.stozo04.openloop.R
* | eye line down to the **chin** | **1.75** |
* | mouth width, at rest | 0.8 |
*
* These are **measured off a real tracked face**, not assumed. An earlier version reasoned them
* from published head statistics (treating one unit as the interpupillary distance) and every lens
* came out ~20% oversized — eye-to-mouth is the larger of the two spans. If a lens ever needs
* These are **measured off a real tracked face**, not assumed. If a lens ever needs
* resizing, re-measure against this table rather than nudging a lens in isolation.
*
* ## The chin row was wrong until 2026-08-16, and it shipped two bugs
*
* This table used to say **chin = 1.00** — which is the same number as the mouth, and that
* coincidence is the tell: the row was the *mouth* wearing the chin's label. A real jaw runs about
* another **0.75 units** below the mouth (stomion→menton ≈ 50 mm against a ≈ 67 mm pupil→stomion
* face unit), putting the chin at **-1.75**.
*
* [PizzaFace] and [Football] were both sized against the wrong number, so both stopped at roughly
* the mouth and left the subject's real chin — and the corners of their real mouth — on show below
* the art. That is what "the mouth looks duplicated" was: the character's composited mouth *and*
* the subject's own, at once. [Broccoli] was never affected because it hangs a stalk far below the
* jaw and covers to -3.88 regardless.
*
* `LensAnchorTest.characterLensesCoverTheWholeHead` now asserts crown-to-chin coverage for every
* character lens, so the class of bug cannot come back silently.
*
* Because they are ratios of the face to itself, one set of numbers holds for every face at every
* distance and angle — there is nothing here to re-tune per device.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -410,12 +410,8 @@ object LensAnchor {
}

/**
* Resolves the three features to lift onto a character: left eye, right eye, mouth.
*
* Sources follow the subject's real landmarks (so a blink, a smile and a head turn all carry
* through); destinations are fixed points in the face frame (so the character's face does not
* inherit the subject's proportions, camera perspective, or a lens-distorted nose). That split
* is the whole trick — expression from the human, geometry from the character.
* Resolves the three features to lift onto a character. Expression from the human (real
* landmarks), geometry from the character (fixed points in the face frame).
*/
fun features(
face: FaceSnapshot,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -783,12 +783,6 @@ class LensSurfaceProcessor(context: Context) : SurfaceProcessor {

/**
* Camera pass — a straight sample of the external OES texture through CameraX's transform.
*
* This used to carry up to two radial bulges (Big Mouth, Bug Eyes). Both lenses are gone,
* and with them the flip into y-down screen space they needed: the shader converted
* `vTexCoord`, warped, and converted back, which is exactly the identity when no warp
* fires. A lens that deforms pixels again wants that scaffolding back — it is in the
* history, not commented out here.
*/
const val CAMERA_FRAGMENT_SHADER = """
#extension GL_OES_EGL_image_external : require
Expand Down
Loading