EyeQ Docs

Using the Android SDK

In-depth guide to using the Perfectly Clear Android SDK — engine lifecycle, image format, scene detection, and memory management

The Android SDK wraps the Perfectly Clear correction engine in a Kotlin/JNI interface backed by native C++ libraries. This page covers the critical integration patterns you need to understand before shipping.

Core objects

The SDK revolves around a small set of classes. Every image correction call involves most of them.

ClassDescription
V10Main SDK interface. Manages the engine lifecycle, image analysis, correction, and creative looks
PFCParamAll correction parameters — core exposure, face tools, noise reduction, color, LUTs
FaceInfoFace bounding box and eye positions from Face Beautification detection
FaceRectAdvanced face geometry with blink, smile, and confidence attributes
SelectiveColorPer-hue-range color adjustment (RAMP channels)

Engine lifecycle

The V10 engine is the central object that holds loaded AI models and configuration. Create it once, reuse it across multiple images, and destroy it when done:

val sdk = V10()
val engine = sdk.createEngine()

// Load models and validate license
sdk.loadAIEngine(engine, apiKey, cert, /* model buffers... */, useGpu = false)
sdk.loadScenePresets(engine, presetBuffer)

// Process images...

sdk.destroyEngine(engine)

Processing pipeline

For each image you want to correct:

  1. calc() — analyze the image and create a profile (contains detected faces, noise level, scene data)
  2. getDetectedScene(profile) — get the AI-detected scene ID
  3. readScenePreset(param, engine, sceneId) — populate a PFCParam with tuned settings for that scene
  4. apply() — apply the corrections to the pixel buffer in place
val profile = sdk.calc(width, height, stride, pixelFormat, pixels, 0, 0, 0, null, engine, 0, 0)
val sceneId = sdk.getDetectedScene(profile)

val param = PFCParam()
sdk.readScenePreset(param, engine, sceneId)

// Optionally adjust any correction params here

pixels.clear()
bitmap.copyPixelsToBuffer(pixels)
val result = sdk.apply(width, height, stride, pixelFormat, pixels, engine, profile, param)

// Write corrected pixels back to bitmap
if (result >= 0) {
    pixels.clear()
    pixels.limit(bitmap.allocationByteCount)
    bitmap.copyPixelsFromBuffer(pixels)
}

sdk.releaseProfile(profile)

Image format

The SDK operates on raw pixel buffers, not Bitmap objects directly. Android Bitmap.Config.ARGB_8888 stores pixels in ABGR native byte order.

Required format

PropertyRequired value
Bitmap configARGB_8888
Pixel format constantV10.PFC_PixelFormat32bppABGR
Bits per pixel32
Stridewidth × 4

Pixel buffer workflow

Always use the SDK's native allocator for pixel buffers:

// Allocate native buffer
val pixels = sdk.allocNativeBuffer(bitmap.allocationByteCount)!!

// Copy pixels from bitmap to buffer before calc()
bitmap.copyPixelsToBuffer(pixels)

// After apply(), copy pixels back before calc() or apply() is called again
pixels.clear()
bitmap.copyPixelsToBuffer(pixels)
sdk.apply(width, height, stride, pixelFormat, pixels, engine, profile, param)

// Copy corrected pixels back to bitmap
pixels.clear()
pixels.limit(bitmap.allocationByteCount)
bitmap.copyPixelsFromBuffer(pixels)

Call pixels.clear() before each copyPixelsToBuffer and before copyPixelsFromBuffer to reset the buffer position. The SDK reads and writes starting at position 0.

Scene detection

Scene detection classifies image content using on-device TFLite models and returns a scene ID used to load optimized correction parameters.

// Analyze the image
val profile = sdk.calc(width, height, stride, pixelFormat, pixels, 0, 0, 0, null, engine, 0, 0)

// Get the detected scene
val sceneId = sdk.getDetectedScene(profile)

// Load optimized parameters for this scene
val param = PFCParam()
sdk.readScenePreset(param, engine, sceneId)

// Optionally populate skin tone settings
sdk.getDetectedSkintoneParams(profile, param)

Scene catalogue

Scenes are identified by integer labels. Common scenes across AI models:

SceneLabelWorkflow
Auto — People1Universal, Pro
Newborn2Universal, Pro
Group Portraits7Universal, Pro
White Backgrounds8Universal, Pro
People at Night9Universal, Pro
Auto — Landscape60Universal, Pro
Animals57Universal, Pro
Food & Drinks58Universal, Pro
Flowers59Universal, Pro
Underwater91Universal, Pro
Black & White100Universal, Pro

Parameter management

PFCParam holds all correction parameters. The default constructor PFCParam() creates an object with default values. Call reset() to restore defaults at any time.

Each correction group has an enable flag that must be set to true for its settings to take effect:

  • core_bEnabled — core image corrections (exposure, contrast, DCF, etc.)
  • fb_bEnabled — face beautification
  • nr_bEnabled — noise reduction
  • re_bEnabled — red eye removal
// Identity — default values
val param = PFCParam()

// Scene-optimized parameters
sdk.readScenePreset(param, engine, sceneId)

// Adjust overall strength (0–200, default 100)
sdk.applyStrengthToParam(param, 120)

// Reset to defaults
param.reset()

Adjusting individual parameters

After loading a preset, fine-tune individual parameters directly:

sdk.readScenePreset(param, engine, sceneId)

// Boost sharpening
param.core_bSharpen = true
param.core_fSharpenScale = 1.5f

// Enable skin smoothing
param.fb_bEnabled = true
param.fb_bSmooth = true
param.fb_iSmoothLevel = 60
param.fb_iSmoothType = V10.SKIN_SMOOTH_TYPE_DEFAULT

sdk.apply(width, height, stride, pixelFormat, pixels, engine, profile, param)

Key correction parameters

The correction parameters are documented in the API reference, and share very similar naming to the C SDK. Some commonly used parameters include:

PropertyTypeRangeDescription
core_iStrengthInt0–150Exposure correction strength
core_bUseAutomaticStrengthSelectionBooleanLet the SDK choose strength automatically
core_iContrastInt0–100Contrast level
core_iLocalContrastInt0–100Local contrast level
core_iVibrancyInt0–100True color calibration level
fb_iSmoothLevelInt0–100Skin smoothing amount
fb_iTeethLevelInt0–100Teeth whitening amount
fb_iEyeCircInt0–100Eye circle reduction amount
fb_iCatchLightInt0–100Catchlight enhancement amount
v3_iDynamicInt0–100Dynamic correction strength
v3_iDynamicWBInt0–100Dynamic white balance strength
v3_lutOutputGUIDStringCreative look GUID (empty = no look)
v3_lutOutputStrengthInt0–200Creative look strength

Custom presets

Load .preset files exported from Perfectly Clear Workbench or Perfectly Clear Complete using readPresets or readPresetsFromStream:

// From a file path on disk
sdk.readPresets(param, "/path/to/my_preset.preset")

// From a String (e.g. loaded from assets)
sdk.readPresetsFromStream(param, presetXmlString)

Memory management

Image processing requires significant native memory. The SDK provides helpers for buffer allocation and memory monitoring.

Native buffer allocation

Always use allocNativeBuffer and freeNativeBuffer for pixel data:

val buffer = sdk.allocNativeBuffer(bitmap.allocationByteCount)

// ... process image ...

sdk.freeNativeBuffer(buffer)

Memory monitoring

val totalMemory = sdk.getMemorySize()
val usedMemory  = sdk.getMemoryUsed()
val freeMemory  = sdk.getMemoryAvailable()

// Limit SDK memory usage to 80% of available
sdk.setMemoryLimit(0.8f)

Profile cleanup

Always call releaseProfile() after you are done with a profile to free native memory:

val profile = sdk.calc(/* ... */)
// ... use profile ...
sdk.releaseProfile(profile)

Failing to call releaseProfile() and freeNativeBuffer() causes native memory leaks that are not tracked by the Android garbage collector.

Face detection results

After calc(), query detected faces for Face Beautification or Face Aware Exposure. See fbFaceCount, getFaceInfo, faeFaceCount, and getFAEFaceRect for the full method signatures.

Face Beautification faces

val faceCount = sdk.fbFaceCount(profile)
val faceInfo = FaceInfo()
for (i in 0 until faceCount) {
    if (sdk.getFaceInfo(profile, faceInfo, i)) {
        // faceInfo.faceLeft, faceTop, faceWidth, faceHeight
        // faceInfo.leftEyeX, leftEyeY, rightEyeX, rightEyeY
    }
}

Face Aware Exposure faces

Use faeFaceCount() and getFAEFaceRect() for richer face data including blink level, smile level, and confidence:

val faeCount = sdk.faeFaceCount(profile)
val faceRect = FaceRect()
for (i in 0 until faeCount) {
    if (sdk.getFAEFaceRect(profile, faceRect, i)) {
        // faceRect.confidence, faceRect.smileLevel, faceRect.blinkLevel
        // Individual eye bounding boxes: eyeLLeft, eyeLTop, etc.
    }
}

Creative looks

Apply creative looks (3D LUTs) by loading a .looks file with loadAddonLooks and setting the output LUT GUID on the param:

// Load looks into the engine (alternative to setAddonPath)
sdk.loadAddonLooks(engine, looksFileBuffer)

// Set the creative look on the param
param.v3_lutOutputGUID = "LOOK_GUID_FROM_SDK"
param.v3_lutOutputStrength = 80
param.v3_iOutLUTcontrast = 0
param.v3_iOutLUTsaturation = 0

sdk.apply(width, height, stride, pixelFormat, pixels, engine, profile, param)

For custom GPU processing, retrieve the raw 3D LUT data with getLooks3DLut:

val lut = FloatArray(16 * 16 * 16 * 3)
sdk.getLooks3DLut(engine, "LOOK_GUID", 100, 0, 0, lut)
// lut contains a 16×16×16 RGB lookup table

Certificate validation

Check your license status at any time with checkCertificate:

val daysLeft = sdk.checkCertificate(apiKey = "YOUR_KEY", certificate = "YOUR_CERT")
// Positive = days remaining
// -1 = expired
// -2 = invalid key/cert combination

Pitfalls and solutions

apply() returns a negative value

Check the return code against the APPLY_* constants. Common causes: APPLY_ERROR_PROFILE_MISSING (forgot to call calc() first), APPLY_INVALID_LICENSE (bad API key or expired certificate), APPLY_BAD_FORMAT (wrong pixel format constant).

Native memory leak / OutOfMemoryError

Every allocNativeBuffer must be paired with freeNativeBuffer, and every calc() profile must be released with releaseProfile(). Native allocations are invisible to the Android GC.

Wrong colors or corrupt output

The Bitmap is not in ARGB_8888 format, or you passed the wrong pixelFormat constant. Android ARGB_8888 requires V10.PFC_PixelFormat32bppABGR.

Scene detection returns unexpected results

The scene detection and skin tone models must be loaded as a matching pair (same UUID). Mixing models from different groups produces unreliable classifications.

No visible effect after apply()

The enable flag for the correction group is false. Verify that core_bEnabled, fb_bEnabled, or the relevant group flag is set to true in the PFCParam.

On this page