Petnow LogoPetnow

Using PetnowCameraHelper

How to implement a fully custom camera UI using PetnowCameraHelper.


Before starting this guide, read the UI Module Overview.

Overview

PetnowCameraHelper is a UI-independent alternative to PetnowCameraFragment. It provides only the camera detection logic without Fragment inheritance, allowing developers to implement a fully custom UI.

PetnowCameraFragment vs PetnowCameraHelper

FeaturePetnowCameraFragmentPetnowCameraHelper
Camera previewBuilt-inImplement yourself
Detection overlayBuilt-in (Lottie)Implement yourself
Camera permissionsAuto-requestedHandle yourself
Lifecycle managementAutomaticManual
Compose compatibleLimitedFully compatible
React Native compatibleNoYes
UI flexibilityOverlay layout100% custom

When to Use

  • Jetpack Compose based apps
  • React Native or Flutter bridges
  • Architecture where Fragment inheritance is difficult
  • When a fully custom camera UI is required

Architecture

PetnowCameraHelper manages the entire camera detection lifecycle but does not handle UI rendering. State changes are delivered via StateFlow, and events via PetnowCameraDetectionListener callbacks.


Basic Integration

Step 1: Create Helper

import io.petnow.ui.PetnowCameraHelper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers

class CameraScreen(private val context: Context) {
    private val scope = CoroutineScope(Dispatchers.Main)
    private val cameraHelper = PetnowCameraHelper(context, scope)
}

Step 2: Initialize

// PetnowUiClient.initialize() must be called beforehand
suspend fun setup() {
    cameraHelper.initialize()
    cameraHelper.initializeDetector()
    cameraHelper.initializeSpecies(isDog = true)  // true: dog, false: cat
}

Step 3: Register Listener

import io.petnow.callback.PetnowCameraDetectionListener
import io.petnow.ui.DetectionCaptureResult
import io.petnow.ui.status.PetnowDetectionStatus

cameraHelper.setDetectionListener(object : PetnowCameraDetectionListener {
    override fun onDetectionStatus(primaryDetectionStatus: PetnowDetectionStatus) {
        // Detection status update (3-second debounce applied)
        when (primaryDetectionStatus) {
            PetnowDetectionStatus.Detected -> showMessage("Detecting...")
            PetnowDetectionStatus.NoObject -> showMessage("Center your pet in the frame")
            PetnowDetectionStatus.TooFarAway -> showMessage("Move closer")
            PetnowDetectionStatus.TooClose -> showMessage("Move farther away")
            else -> showMessage(primaryDetectionStatus.name)
        }
    }

    override fun onDetectionProgress(progress: Int) {
        // Progress update (0-100)
        updateProgressBar(progress)
    }

    override fun onDetectionFinished(result: DetectionCaptureResult) {
        when (result) {
            is DetectionCaptureResult.Success -> {
                // result.noseImageFiles: Nose print image files
                // result.faceImageFiles: Face image files
                handleSuccess(result)
            }
            is DetectionCaptureResult.Fail -> {
                handleFailure()
            }
        }
    }
})

Step 4: Open Camera and Start Detection

import java.util.UUID

suspend fun startCapture(surface: Surface, captureSessionId: UUID) {
    // Set configuration (optional)
    val config = DetectionConfiguration(
        species = PetSpecies.DOG,
        purpose = DetectionPurpose.PET_PROFILE_REGISTRATION,
        requestImageCount = 5
    )
    cameraHelper.setDetectionConfiguration(config)

    // Open camera
    cameraHelper.openCamera(surface, captureSessionId)

    // Start detection session
    cameraHelper.startDetectionSession()
}

Step 5: Observe State (StateFlow)

In addition to callbacks, you can observe state via StateFlow. This is especially useful in Compose.

// Observe StateFlow
scope.launch {
    cameraHelper.state.collect { state ->
        // state.progressPercent: Progress (0-100)
        // state.detectionStatusList: Current frame's detection status list
        // state.isDetectionFinished: Whether detection is complete
        // state.isCameraOpened: Camera open state
        // state.isDetectionRunning: Whether detection is running
        // state.isTemporaryPause: Temporary pause state
        // state.currentCameraId: Current camera ID (front/back)
    }
}

Step 6: Release Resources

fun cleanup() {
    cameraHelper.closeCamera()
    cameraHelper.release()
}

Detection Session Control

Auto Capture (Default)

cameraHelper.startDetectionSession()

Nose-only Capture

cameraHelper.startNoseOnlyDetectionSession()

Manual Capture

// Start manual mode
cameraHelper.startManualDetectionSession(maxImageCount = 5)

// Call on each capture button click
cameraHelper.captureManually(captureCountPerClick = 1)

Retake

cameraHelper.startDetectionSession()

Pause / Resume Detection

You can temporarily pause and resume detection. Useful while displaying overlay UI (e.g., tips sheet).

// Pause detection (progress preserved)
cameraHelper.pauseDetection()

// Resume detection (continues from pause point)
cameraHelper.resumeDetection()

pauseDetection() pauses while preserving progress. resumeDetection() resumes from where it was paused. For retake (reset progress), use startDetectionSession().


Temporary Pause

When using external UI like a gallery picker, you may need to prevent the camera from closing.

// Before opening gallery
cameraHelper.setTemporaryPause(true)

// After returning from gallery
cameraHelper.setTemporaryPause(false)

Check this state in your lifecycle management code:

override fun onPause() {
    super.onPause()
    // Only close camera when not in temporary pause
    if (!cameraHelper.state.value.isTemporaryPause) {
        cameraHelper.closeCamera()
    }
}

Camera Switching

val result = cameraHelper.switchCamera()
result.onSuccess {
    // Front ↔ Back switch successful
}
result.onFailure { e ->
    // e.g., device has no front camera
}

Bracketing Mode

Set bracketing mode for automatic exposure compensation adjustment.

// Must be set before starting detection session
cameraHelper.setBracketingMode(enabled = true)

// Check current state
val isEnabled = cameraHelper.isBracketingModeEnabled()

Bracketing mode must be set before calling startDetectionSession(). Changes after session start will take effect from the next session.


Sound Playback

import io.petnow.ui.sound.SoundType

// Play sound
val streamId = cameraHelper.playSound(SoundType.CAPTURE)

// Stop sound
cameraHelper.stopSound(streamId)

Jetpack Compose Example

@Composable
fun PetnowCameraScreen(captureSessionId: UUID) {
    val context = LocalContext.current
    val scope = rememberCoroutineScope()
    
    val cameraHelper = remember {
        PetnowCameraHelper(context, scope)
    }
    
    val cameraState by cameraHelper.state.collectAsState()

    DisposableEffect(Unit) {
        scope.launch {
            cameraHelper.initialize()
            cameraHelper.initializeDetector()
            cameraHelper.initializeSpecies(isDog = true)
        }
        
        onDispose {
            cameraHelper.closeCamera()
            cameraHelper.release()
        }
    }

    // Set listener
    LaunchedEffect(Unit) {
        cameraHelper.setDetectionListener(object : PetnowCameraDetectionListener {
            override fun onDetectionStatus(status: PetnowDetectionStatus) { /* ... */ }
            override fun onDetectionProgress(progress: Int) { /* ... */ }
            override fun onDetectionFinished(result: DetectionCaptureResult) { /* ... */ }
        })
    }

    Column(modifier = Modifier.fillMaxSize()) {
        // Camera preview (wrap SurfaceView with AndroidView)
        AndroidView(
            factory = { ctx ->
                SurfaceView(ctx).apply {
                    holder.addCallback(object : SurfaceHolder.Callback {
                        override fun surfaceCreated(holder: SurfaceHolder) {
                            scope.launch {
                                cameraHelper.openCamera(holder.surface, captureSessionId)
                                cameraHelper.startDetectionSession()
                            }
                        }
                        override fun surfaceChanged(h: SurfaceHolder, f: Int, w: Int, ht: Int) {}
                        override fun surfaceDestroyed(holder: SurfaceHolder) {
                            cameraHelper.closeCamera()
                        }
                    })
                }
            },
            modifier = Modifier.weight(1f)
        )

        // Progress bar
        LinearProgressIndicator(
            progress = { cameraState.progressPercent / 100f },
            modifier = Modifier.fillMaxWidth()
        )

        // Detection status text
        Text(
            text = cameraState.detectionStatusList.firstOrNull()?.name ?: "",
            modifier = Modifier.padding(16.dp),
            textAlign = TextAlign.Center
        )
    }
}

False-Negative Collection

Collect false-negative images to improve detection quality.

import io.petnow.callback.PetnowCameraFalseNegativeListener
import io.petnow.ui.model.FalseNegativeUiInfo

// Enable false-negative collection
cameraHelper.setFalseNegativeCollect(enable = true, intervalMillis = 1000L)

// Set listener
cameraHelper.setFalseNegativeListener(object : PetnowCameraFalseNegativeListener {
    override fun onDetectionFalseNegative(falseNegativeInfo: FalseNegativeUiInfo) {
        // Process false-negative data
    }
})

API Summary

MethodDescription
initialize()Initialize monitoring service
initializeDetector()Initialize ObjectDetector
initializeSpecies(isDog)Set dog/cat mode
openCamera(surface, captureSessionId)Open camera
closeCamera()Close camera
startDetectionSession()Start auto detection session
startNoseOnlyDetectionSession()Start nose-only capture session
startManualDetectionSession(maxImageCount)Start manual capture session
captureManually(captureCountPerClick)Trigger manual capture
resumeDetection()Resume paused detection
pauseDetection()Pause detection (preserves progress)
setTemporaryPause(temporary)Set temporary pause state
switchCamera()Switch front/back camera
setBracketingMode(enabled)Set bracketing mode
setDetectionConfiguration(config)Apply detection configuration
setDetectionListener(listener)Register detection listener
setFalseNegativeCollect(enable, intervalMillis)False-negative collection
playSound(soundType) / stopSound(id)Play/stop sound
release()Release resources

Next Steps

On this page