Circadify

Scan Overlay

Render the thermal face-mesh scan glow with CircadifyOverlayView, drive it from onScanFace, and retheme it with CircadifyScanStyle.

The Android SDK ships CircadifyOverlayView — the thermal face-mesh scan glow that makes a Circadify scan recognizable. It paints a heat glow onto the live face mesh and adds a stationary gold guide oval, tracking the subject and pulsing hotter while measuring. The SDK stays headless: you own the layout, the preview, and the buttons, and drop the overlay on top.

The geometry that drives it — CircadifyScanFace — arrives on the onScanFace callback, so you can either mount the bundled overlay as-is or paint your own glow from the same projected anchors.

Prefer the bundled overlay — don't rebuild it

CircadifyOverlayView already renders the thermal/heat-glow face overlay for you. Mount it instead of hand-rolling an overlay from the raw onLandmarks callback. Only reach for a custom overlay when you genuinely need one — and then drive it from onScanFace, which gives you the same projected geometry the bundled view uses.

What the glow looks like

  • The full 468-point / 880-triangle MediaPipe face mesh.
  • A thermal ramp (gold → orange → red core) that sweeps region-by-region across the forehead and cheeks, computed in 3D so it conforms to the face.
  • A stationary gold guide oval ("hold still inside the oval") shown under the glow. The glow itself only appears once a face is detected — no resting placeholder.
  • A subtler, slower pulse while framing; a brighter, faster pulse during a measurement.

The heat-glow is rendered on Android with Canvas.drawVertices.

Adding the overlay

CircadifyOverlayView is a plain View. Put it on top of your CameraX PreviewView with identical bounds — the simplest way is a FrameLayout with both children set to MATCH_PARENT. Use PreviewView.ScaleType.FILL_CENTER so the preview matches the overlay's aspect-fill projection.

  1. Stack the PreviewView and a CircadifyOverlayView in a FrameLayout, overlay last (on top), both MATCH_PARENT.

  2. Feed the overlay live geometry from the onScanFace callback — assign face straight to overlay.scanFace (it is null when no face is detected).

  3. Flag the measurement: set overlay.isScanning = true while measureVitals() runs for the hotter, faster glow, and reset when it finishes.

import androidx.camera.view.PreviewView
import com.circadify.sdk.CircadifyCallbacks
import com.circadify.sdk.CircadifyConfig
import com.circadify.sdk.CircadifyOverlayView
 
val previewView = PreviewView(this).apply {
    scaleType = PreviewView.ScaleType.FILL_CENTER
}
val overlay = CircadifyOverlayView(this)
 
// Overlay sits on top of the preview with identical bounds.
val cameraContainer = FrameLayout(this).apply {
    addView(previewView, FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT))
    addView(overlay, FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT))
}
 
val sdk = CircadifySDK(
    context = this,
    config = CircadifyConfig(
        apiKey = "ck_test_your_key_here",
        callbacks = CircadifyCallbacks(
            // null when no face is detected — the glow hides.
            onScanFace = { face -> overlay.scanFace = face },
        ),
    ),
)
kotlin

Drive the brighter glow around the measurement itself:

overlay.isScanning = true
try {
    val result = sdk.measureVitals(
        MeasurementOptions(lifecycleOwner = this, previewView = previewView),
    )
    // ...show vitals...
} finally {
    overlay.isScanning = false
    overlay.scanFace = null
}
kotlin
Note

CircadifyOverlayView does not intercept touches, so it never steals taps from your UI. It animates itself while attached to the window, only self-redraws while a face is present (so it doesn't burn battery idle), and respects the system remove animations accessibility setting.

Theming

Retheme the overlay by assigning a CircadifyScanStyle to overlay.style. The default palette is a warm thermal ramp — gold → orange → red — that pulses across the face. Override any field to recolor or re-time the glow. Colors are ARGB ints (e.g. 0xFFF2B340.toInt()).

import com.circadify.sdk.CircadifyScanStyle
 
// A cool cyan ramp instead of the default thermal one.
overlay.style = CircadifyScanStyle(
    coreColor = 0xFF1AB3FF.toInt(),
    hotColor = 0xFF1F80FF.toInt(),
    midColor = 0xFF1FB3FF.toInt(),
    goldColor = 0xFF40D2F2.toInt(),
    ovalColor = 0xFF40D2F2.toInt(),
)
kotlin
FieldTypeDefaultDescription
coreColorInt (ARGB)0xFFFF331ARed core — only fully present at peak scanning intensity.
hotColorInt (ARGB)0xFFFF401FHot orange.
midColorInt (ARGB)0xFFFF801FMid orange.
goldColorInt (ARGB)0xFFF2B340Gold — the resting/idle tone.
ovalColorInt (ARGB)0xFFF2B340Thin guide-oval outline drawn under the glow.
scanRegionDurationSecondsDouble2.0Seconds each region stays lit while measuring (faster, hotter). Must be > 0.
idleRegionDurationSecondsDouble3.5Seconds each region stays lit while idle/framing (slower, calmer). Must be > 0.
scanPeakDouble1.0Peak glow intensity while measuring. Range 0.0..1.0.
idlePeakDouble0.4Peak glow intensity while idle/framing. Range 0.0..1.0.

Custom overlay

Prefer to draw your own overlay? onScanFace delivers a CircadifyScanFace with the same geometry the bundled view paints. All points are normalized [0,1] in the detector's upright, unmirrored image space (top-left origin) — the same space the measurement pipeline uses. Project them into view coordinates yourself: aspect-fill crop with imageAspect, then flip x when isMirrored (true for the front camera).

callbacks = CircadifyCallbacks(
    onScanFace = { face ->
        if (face == null) {
            myOverlay.clear()
        } else {
            myOverlay.update(face) // project + draw your own glow / reticle
        }
    },
)
kotlin
FieldTypeDescription
foreheadPointFGlow anchor over the forehead (mean of the forehead ROI landmarks).
leftCheekPointFGlow anchor over the subject's left cheek (image-left; mirrored for display).
rightCheekPointFGlow anchor over the subject's right cheek.
ovalCenterPointFCenter of the face oval (padded bounding ellipse of all landmarks).
ovalSizeSizeFSize of the face oval, in normalized image units.
imageAspectFloatAspect ratio (width / height) of the image the landmarks were detected in.
isMirroredBooleanWhether the preview shows the image mirrored (true for the front camera).
landmarksList<Landmark>All 468 face-mesh landmarks (normalized, upright, unmirrored) backing this geometry.

Next Steps