voxa_beauty

Real-time beauty filtering for voxa_rtc_engine — skin smoothing, tone and warmth on the camera capture path.

No licence, no subscription, no account, no model files. The entire effect is a shader.

Setup

dependencies:
  voxa_beauty: ^0.3.2

That is the whole integration. No gradle edit, no MainActivity change, no ProGuard rule, no native code — the filter registers itself.

final beauty = VoxaBeauty(engine);
await beauty.enable();              // sensible defaults
await beauty.setSmoothing(0.7);     // or drive it from a slider
await beauty.disable();

Gate your control on beauty.isSupported rather than assuming — it is false on platforms this package does not cover yet, so you show no toggle instead of one that does nothing.

Settings

All are 0..1 and take effect on the next frame, so they are cheap to drive from a slider.

Default Notes
setSmoothing 0.55 Around 0.5 reads as a good camera. Past ~0.8 skin starts to look plastic, which is rarely what people want even when they ask for it.
setBrightness 0.12 Lifts shadows rather than the whole curve, so highlights don't wash out.
setWarmth 0.10 Nudges towards warmer tones.
setSaturation 0.10 Extra colour.

Why it's effectively free

The filter runs in the graphics context the capturer and the hardware encoder already share. A frame arrives as a GPU texture, goes through one shader pass, and leaves as a GPU texture — no readback, no CPU copy, no pixel ever touching system memory, and nothing crossing into Dart.

Measured on a Pixel 6 at 1024×576: 24 fps with beauty on, which is the camera's own capture rate — the same as with no filter at all.

There is no per-frame millisecond figure here on purpose. GL commands are queued rather than executed, so timing the draw call measures how long it took to submit work, not what the GPU spent on it. Sustained capture rate is the honest number.

Makeup

Lipstick, blush and eyeshadow track the face using MediaPipe face landmarks — on-device, Apache 2.0, no account and no per-user fee.

await beauty.setLipColour(const Color(0xFFC7385A));
await beauty.setLipstick(0.7);
await beauty.setBlush(0.5);
await beauty.setEyeshadow(0.4);
await beauty.setLipstick(0);      // tracking stops once all three are 0
Default shade Notes
setLipstick 0xFFC7385A Traces the lip outline as geometry.
setBlush 0xFFF5858F A radial fade on the apple of each cheek. Around 0.4 reads as colour in the cheeks; past ~0.7 it looks applied rather than natural.
setEyeshadow 0xFF946BA3 An arc over each lid, densest at the lash line and gone by the crease.

All three multiply rather than paint, so the feature underneath keeps its own highlights and creases instead of looking like a sticker. A multiply can only darken, which is why the blush and eyeshadow defaults are pastel — a saturated shade over an area that size stops reading as makeup.

Blush and eyeshadow are sized from the tracked face rather than in fixed pixels, so they hold still relative to you as you move towards and away from the camera instead of growing and shrinking.

Makeup is the one thing here that is not free. Any strength above zero starts face tracking, which reads back a small copy of each frame and runs a model on it. Detection runs off the capture thread against a downscaled, upright copy and the shader always uses the newest result rather than waiting, so measured frame rate is unchanged at 24 fps — but it is real work, and it stays off until a user chooses a colour. One tracker serves all three, so the second and third cost nothing beyond the first.

Landmarks are smoothed over time before use. Raw ones wobble even on a still face, and makeup drawn straight onto them swims on the face.

How closely it follows

Inference takes about 50 ms on a Pixel 6, which is longer than the camera's own frame interval — so roughly every other frame arrives with nowhere to go. Those frames are dropped, before the readback, rather than queued. Queueing them is free per frame and ruinous in aggregate: the backlog grows by the difference between the two rates forever, and it is the reason makeup on a filter can look progressively more detached the longer you use it.

What is left is about 100 ms, and rather than chase it with a faster model it is predicted away. The One Euro filter already estimates how fast every landmark is travelling in order to decide how hard to smooth it, and that same velocity says where the point should be by the time the frame is drawn. The trade is a small overshoot when your head changes direction mid-turn, which is much less noticeable than a constant lag. It also decouples makeup from the detection rate — landmarks arrive about 23 times a second, and makeup moves at the camera's full 24 fps regardless.

Prediction has a cost of its own, though, and it is worth knowing about because it is the other thing people notice. Noise in the velocity is multiplied by the horizon before it reaches the screen, so landmark wobble far too small to see becomes visible shimmer on a moving face — and only on a moving face, because at rest there is no velocity to multiply. Three things keep it down, and all three avoid buying quiet with lag: the detector holds two frames so it never idles between camera frames, detection runs at 384 px so the crop the model scores has real pixels in it, and the velocities are averaged into a single movement for the whole face, which divides their noise by the square root of their number for free.

Smoothing harder is the obvious fix and it does not work. It was tried and measured: heavier smoothing lags more, the horizon has to grow to cover that lag, and a longer horizon multiplies velocity error by more than the smoothing removed. Short horizons matter more here than clean positions.

Five numbers in the log say whether all this is working, and none is inferable from the others:

VoxaBeauty: 24.0 fps with beauty (smoothing 0.55), landmarks 92 ms old, grab 4.8 ms, step 2.4 px, jerk 1.2 px, shift 8.8 px
VoxaBeauty: 23.0 detections/sec, inference 41 ms

landmarks N ms old should be steady and around one inference — if it climbs, frames are being queued somewhere. jerk is how much the drawn movement changes between frames, against step, how far it moves; well below step is smooth, comparable to it is shimmer. Judge it while barely moving, since a genuine head shake has large jerk of its own.

shift is how far prediction moved the makeup from where it was actually detected, and it is the only one of these that can tell a lead from a lag — step and jerk look the same whether makeup trails the face or runs ahead of it. Expect roughly step times the horizon over the frame interval, so three or four times step here. Several times that means the speed driving the extrapolation is wrong, not that you are moving fast.

What it does, and what it doesn't

Smoothing is an edge-preserving blur — a sparse bilateral approximation — gated by a skin-tone mask in YCbCr. The mask is what makes it look deliberate: blur the whole frame and the room goes soft and it reads as a cheap filter, but restrict it to skin and eyes, hair and background stay sharp.

Beyond the three makeup effects above there is no mask or prop support yet, and no background replacement. The tracking those need is in place — what is missing is a filter format and the artwork to fill it. See below.

Downloadable filters

There is no filter file format here yet. Effects are code, not data, so adding one means changing this package rather than dropping in a download. A format (a manifest plus textures painted to MediaPipe's canonical face UV layout) is the natural next step, and the tracking it would sit on is already working.

For prop and elaborate makeup effects today, see voxa_deepar — the two register under different names and can both be installed.

Platforms

Platform Status
Android Supported
iOS Not yet — isSupported returns false rather than failing at runtime

Notes

Frames that arrive on the CPU rather than as a texture pass through untouched: uploading, filtering and reading back would cost more than the effect is worth. In practice camera frames are textures.

Libraries

voxa_beauty
Real-time beauty filters for VoxaRTC.