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.
| Class | Description |
|---|---|
V10 | Main SDK interface. Manages the engine lifecycle, image analysis, correction, and creative looks |
PFCParam | All correction parameters — core exposure, face tools, noise reduction, color, LUTs |
FaceInfo | Face bounding box and eye positions from Face Beautification detection |
FaceRect | Advanced face geometry with blink, smile, and confidence attributes |
SelectiveColor | Per-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:
calc()— analyze the image and create a profile (contains detected faces, noise level, scene data)getDetectedScene(profile)— get the AI-detected scene IDreadScenePreset(param, engine, sceneId)— populate aPFCParamwith tuned settings for that sceneapply()— 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
| Property | Required value |
|---|---|
| Bitmap config | ARGB_8888 |
| Pixel format constant | V10.PFC_PixelFormat32bppABGR |
| Bits per pixel | 32 |
| Stride | width × 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:
| Scene | Label | Workflow |
|---|---|---|
| Auto — People | 1 | Universal, Pro |
| Newborn | 2 | Universal, Pro |
| Group Portraits | 7 | Universal, Pro |
| White Backgrounds | 8 | Universal, Pro |
| People at Night | 9 | Universal, Pro |
| Auto — Landscape | 60 | Universal, Pro |
| Animals | 57 | Universal, Pro |
| Food & Drinks | 58 | Universal, Pro |
| Flowers | 59 | Universal, Pro |
| Underwater | 91 | Universal, Pro |
| Black & White | 100 | Universal, 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 beautificationnr_bEnabled— noise reductionre_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:
| Property | Type | Range | Description |
|---|---|---|---|
core_iStrength | Int | 0–150 | Exposure correction strength |
core_bUseAutomaticStrengthSelection | Boolean | — | Let the SDK choose strength automatically |
core_iContrast | Int | 0–100 | Contrast level |
core_iLocalContrast | Int | 0–100 | Local contrast level |
core_iVibrancy | Int | 0–100 | True color calibration level |
fb_iSmoothLevel | Int | 0–100 | Skin smoothing amount |
fb_iTeethLevel | Int | 0–100 | Teeth whitening amount |
fb_iEyeCirc | Int | 0–100 | Eye circle reduction amount |
fb_iCatchLight | Int | 0–100 | Catchlight enhancement amount |
v3_iDynamic | Int | 0–100 | Dynamic correction strength |
v3_iDynamicWB | Int | 0–100 | Dynamic white balance strength |
v3_lutOutputGUID | String | — | Creative look GUID (empty = no look) |
v3_lutOutputStrength | Int | 0–200 | Creative 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 tableCertificate 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 combinationPitfalls 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.