LiteRT를 사용한 Android의 실시간 가속 이미지 분할

1. 시작하기 전에

코드를 직접 입력하면 근육 기억을 형성하고 자료에 대한 이해를 심화할 수 있습니다. 복사하여 붙여넣기는 시간을 절약할 수 있지만, 이 방법을 활용하면 장기적으로 효율성을 높이고 코딩 기술을 강화할 수 있습니다.

이 Codelab에서는 TensorFlow Lite용 Google의 새로운 런타임인 LiteRT를 사용하여 실시간 카메라 피드에서 실시간 이미지 세분화를 실행하는 Android 애플리케이션을 빌드하는 방법을 알아봅니다. 시작 Android 애플리케이션을 가져와 이미지 세분화 기능을 추가합니다. 전처리, 추론, 후처리 단계도 살펴볼 예정입니다. 실습할 내용은 다음과 같습니다.

  • 이미지를 실시간으로 분할하는 Android 앱을 빌드합니다.
  • 선행 학습된 LiteRT 이미지 세분화 모델을 통합합니다.
  • 모델의 입력 이미지를 전처리합니다.
  • CPU 및 GPU 가속을 위해 LiteRT 런타임을 사용합니다.
  • 모델의 출력을 처리하여 분할 마스크를 표시하는 방법을 이해합니다.
  • 전면 카메라를 조정하는 방법을 이해합니다.

결과적으로 아래 이미지와 유사한 항목이 생성됩니다.

완성된 앱

기본 요건

이 Codelab은 머신러닝 경험을 쌓고 싶은 숙련된 모바일 개발자를 위해 설계되었습니다. 다음을 잘 알고 있어야 합니다.

  • Kotlin 및 Android 스튜디오를 사용한 Android 개발
  • 이미지 처리의 기본 개념

학습할 내용

  • Android 애플리케이션에서 LiteRT 런타임을 통합하고 사용하는 방법
  • 선행 학습된 LiteRT 모델을 사용하여 이미지 세분화를 실행하는 방법
  • 모델의 입력 이미지를 전처리하는 방법
  • 모델의 추론을 실행하는 방법
  • 분할 모델의 출력을 처리하여 결과를 시각화하는 방법
  • 실시간 카메라 피드 처리에 CameraX를 사용하는 방법

필요한 항목

  • Android 스튜디오 최신 버전 (v2025.1.1에서 테스트됨)
  • 실제 Android 기기 Galaxy 및 Pixel 기기에서 테스트하는 것이 가장 좋습니다.
  • 샘플 코드 (GitHub에서 가져옴)
  • Kotlin을 사용한 Android 개발에 관한 기본 지식

2. 이미지 분할

이미지 분할은 이미지를 여러 세그먼트 또는 영역으로 분할하는 컴퓨터 비전 작업입니다. 객체 주위에 경계 상자를 그리는 객체 감지와 달리 이미지 분할은 이미지의 모든 단일 픽셀에 특정 클래스 또는 라벨을 할당합니다. 이를 통해 이미지 콘텐츠를 훨씬 더 자세하고 세부적으로 파악하여 각 객체의 정확한 모양과 경계를 알 수 있습니다.

예를 들어 '사람'이 상자 안에 있다는 것만 아는 대신 해당 사람에 속하는 픽셀을 정확히 알 수 있습니다. 이 튜토리얼에서는 사전 학습된 머신러닝 모델을 사용하여 Android 기기에서 실시간 이미지 세분화를 실행하는 방법을 보여줍니다.

세분화 예

LiteRT: 온디바이스 ML의 한계 뛰어넘기

휴대기기에서 실시간 고화질 분할을 지원하는 핵심 기술은 LiteRT입니다. TensorFlow Lite를 위한 Google의 차세대 고성능 런타임인 LiteRT는 기본 하드웨어에서 최고의 성능을 얻을 수 있도록 설계되었습니다.

이는 GPU (그래픽 처리 장치) 및 NPU (신경망 처리 장치)와 같은 하드웨어 가속기를 지능적이고 최적화된 방식으로 사용하여 달성됩니다. 범용 CPU에서 이러한 특수 프로세서로 세그먼트 모델의 집중적인 컴퓨팅 워크로드를 오프로드함으로써 LiteRT는 추론 시간을 크게 줄입니다. 이러한 가속 덕분에 실시간 카메라 피드에서 복잡한 모델을 원활하게 실행할 수 있으며, 휴대전화에서 직접 머신러닝으로 달성할 수 있는 범위가 확대됩니다. 이 수준의 성능이 없으면 실시간 세분화가 너무 느리고 끊겨서 좋은 사용자 경험을 제공할 수 없습니다.

3. 설정

저장소 클론

먼저 LiteRT 저장소를 클론합니다.

git clone https://github.com/google-ai-edge/LiteRT.git

LiteRT/litert/samples/image_segmentation은 필요한 모든 리소스가 있는 디렉터리입니다. 이 Codelab에서는 kotlin_cpu_gpu/android_starter 프로젝트만 있으면 됩니다. 막히는 부분이 있으면 완성된 프로젝트를 검토하세요. kotlin_cpu_gpu/android

파일 경로 참고사항

이 튜토리얼에서는 Linux/macOS 형식으로 파일 경로를 지정합니다. Windows를 사용하는 경우 경로를 적절하게 조정해야 합니다.

Android 스튜디오 프로젝트 뷰와 표준 파일 시스템 뷰의 차이점도 알아두어야 합니다. Android 스튜디오 프로젝트 뷰는 Android 개발을 위해 구성된 프로젝트 파일의 구조화된 표현입니다. 이 튜토리얼의 파일 경로는 Android 스튜디오 프로젝트 뷰의 경로가 아닌 파일 시스템 경로를 나타냅니다.

시작 앱 가져오기

먼저 Android 스튜디오로 시작 앱을 가져오겠습니다.

  1. Android 스튜디오를 열고 Open을 선택합니다.

Android 스튜디오 열기

  1. kotlin_cpu_gpu/android_starter 디렉터리로 이동하여 엽니다.

Android Starter

앱에서 모든 종속 항목을 사용할 수 있도록 하려면 가져오기 프로세스가 완료된 후 프로젝트를 Gradle 파일과 동기화해야 합니다.

  1. Android 스튜디오 툴바에서 Sync Project with Gradle Files를 선택합니다.

메뉴 동기화

  1. 이 단계를 건너뛰지 마세요. 이 단계가 작동하지 않으면 튜토리얼의 나머지 부분이 의미가 없습니다.

시작 앱 실행

이제 Android 스튜디오로 프로젝트를 가져왔으므로 앱을 처음으로 실행할 수 있습니다.

USB를 통해 Android 기기를 컴퓨터에 연결하고 Android 스튜디오 툴바에서 Run을 클릭합니다.

실행 버튼

기기에서 앱이 실행됩니다. 실시간 카메라 피드가 표시되지만 아직 세분화는 진행되지 않습니다. 이 튜토리얼에서 수정하는 모든 파일은 LiteRT/litert/samples/image_segmentation/kotlin_cpu_gpu/android_starter/app/src/main/java/com/google/aiedge/examples/image_segmentation 디렉터리 아래에 있습니다. 이제 Android 스튜디오에서 이 디렉터리를 재구성하는 이유를 알 수 있습니다 😃.

프로젝트 디렉터리

ImageSegmentationHelper.kt, MainViewModel.kt, view/SegmentationOverlay.kt 파일에도 TODO 주석이 표시됩니다. 다음 단계에서는 이러한 TODO를 채워 이미지 세분화 기능을 구현합니다.

4. 시작 앱 이해

시작 앱에는 이미 기본 UI와 카메라 처리 로직이 있습니다. 다음은 주요 파일에 대한 간략한 개요입니다.

  • app/src/main/java/com/google/aiedge/examples/image_segmentation/MainActivity.kt: 애플리케이션의 기본 진입점입니다. Jetpack Compose를 사용하여 UI를 설정하고 카메라 권한을 처리합니다.
  • app/src/main/java/com/google/aiedge/examples/image_segmentation/MainViewModel.kt: 이 ViewModel은 UI 상태를 관리하고 이미지 세분화 프로세스를 오케스트레이션합니다.
  • app/src/main/java/com/google/aiedge/examples/image_segmentation/ImageSegmentationHelper.kt: 여기에 이미지 세분화의 핵심 로직을 추가합니다. 모델 로드, 카메라 프레임 처리, 추론 실행을 처리합니다.
  • app/src/main/java/com/google/aiedge/examples/image_segmentation/view/CameraScreen.kt: 이 컴포저블 함수는 카메라 미리보기와 세그먼트 오버레이를 표시합니다.
  • app/src/main/assets/selfie_multiclass.tflite: 사용할 사전 학습된 TensorFlow Lite 이미지 분할 모델입니다.

5. LiteRT 이해 및 종속 항목 추가

이제 시작 앱에 이미지 분할 기능을 추가해 보겠습니다.

1. LiteRT 종속 항목 추가

먼저 LiteRT 라이브러리를 프로젝트에 추가해야 합니다. 이는 Google의 최적화된 런타임으로 온디바이스 머신러닝을 사용 설정하는 데 중요한 첫 번째 단계입니다.

app/build.gradle.kts 파일을 열고 dependencies 블록에 다음 줄을 추가합니다.

// LiteRT for on-device ML
implementation(libs.litert)

종속 항목을 추가한 후 Android 스튜디오의 오른쪽 상단에 표시되는 지금 동기화 버튼을 클릭하여 프로젝트를 Gradle 파일과 동기화합니다.

지금 동기화

2. 주요 LiteRT API 이해

열기 ImageSegmentationHelper.kt

구현 코드를 작성하기 전에 사용할 LiteRT API의 핵심 구성요소를 이해하는 것이 중요합니다. com.google.ai.edge.litert 패키지에서 가져오고 있는지 확인하고 ImageSegmentationHelper.kt의 상단에 다음 가져오기를 추가합니다.

import com.google.ai.edge.litert.Accelerator
import com.google.ai.edge.litert.CompiledModel
  • CompiledModel: TFLite 모델과 상호작용하기 위한 중앙 클래스입니다. 특정 하드웨어 가속기 (예: CPU 또는 GPU)에 맞게 사전 컴파일되고 최적화된 모델을 나타냅니다. 이 사전 컴파일은 더 빠르고 효율적인 추론을 지원하는 LiteRT의 핵심 기능입니다.
  • CompiledModel.Options: 이 빌더 클래스를 사용하여 CompiledModel를 구성합니다. 가장 중요한 설정은 모델을 실행하는 데 사용할 하드웨어 가속기를 지정하는 것입니다.
  • Accelerator: 이 열거형을 사용하면 추론을 위한 하드웨어를 선택할 수 있습니다. 시작 프로젝트는 이미 다음 옵션을 처리하도록 구성되어 있습니다.
    • Accelerator.CPU: 기기의 CPU에서 모델을 실행합니다. 가장 보편적으로 호환되는 옵션입니다.
    • Accelerator.GPU: 기기의 GPU에서 모델을 실행합니다. 이는 이미지 기반 모델의 CPU보다 훨씬 빠른 경우가 많습니다.
  • 입력 및 출력 버퍼 (TensorBuffer): LiteRT는 모델 입력 및 출력에 TensorBuffer를 사용합니다. 이렇게 하면 메모리를 세밀하게 제어하고 불필요한 데이터 복사를 방지할 수 있습니다. model.createInputBuffers()model.createOutputBuffers()를 사용하여 CompiledModel 인스턴스에서 직접 이러한 버퍼를 가져온 다음 입력 데이터를 버퍼에 쓰고 버퍼에서 결과를 읽습니다.
  • model.run(): 추론을 실행하는 함수입니다. 입력 및 출력 버퍼를 전달하면 LiteRT가 선택한 하드웨어 가속기에서 모델을 실행하는 복잡한 작업을 처리합니다.

6. 초기 ImageSegmentationHelper 구현 완료

이제 코드를 작성할 차례입니다. ImageSegmentationHelper.kt의 초기 구현을 완료합니다. 여기에는 LiteRT 모델을 보유하도록 Segmenter 비공개 클래스를 설정하고 이를 올바르게 해제하도록 cleanup() 함수를 구현하는 작업이 포함됩니다.

  1. Segmenter 클래스와 cleanup() 함수 완료하기: ImageSegmentationHelper.kt 파일에는 Segmenter라는 비공개 클래스와 cleanup()라는 함수의 스켈레톤이 있습니다. 먼저 모델을 보유하도록 생성자를 정의하고, 입력/출력 버퍼의 속성을 만들고, 모델을 해제하는 close() 메서드를 추가하여 Segmenter 클래스를 완성합니다. 그런 다음 이 새 close() 메서드를 호출하도록 cleanup() 함수를 구현합니다. 기존 Segmenter 클래스와 cleanup() 함수를 다음으로 바꿉니다(~83번째 줄).
    private class Segmenter(
        // Add this argument
        private val model: CompiledModel,
        private val coloredLabels: List<ColoredLabel>,
    ) {
        // Add these private vals
        private val inputBuffers: = model.createInputBuffers()
        private val outputBuffers: = model.createOutputBuffers()
    
        fun cleanup() {
          // cleanup buffers
          inputBuffers.forEach { it.close() }
          outputBuffers.forEach { it.close() }
          // cleanup model
          model.close()
        }
    }
    
  2. toAccelerator 메서드 정의: 이 메서드는 가속기 메뉴에서 정의된 가속기 enum을 가져온 LiteRT 모듈에 특정한 가속기 enum에 매핑합니다 (~225번째 줄).
    fun toAccelerator(acceleratorEnum: AcceleratorEnum): Accelerator {
      return when (acceleratorEnum) {
        AcceleratorEnum.CPU -> Accelerator.CPU
        AcceleratorEnum.GPU -> Accelerator.GPU
      }
    }
    
  3. CompiledModel 초기화: 이제 initSegmenter 함수를 찾습니다. 여기에서 CompiledModel 인스턴스를 만들고 이를 사용하여 이제 정의된 Segmenter 클래스를 인스턴스화합니다. 이 코드는 지정된 가속기 (CPU 또는 GPU)로 모델을 설정하고 추론을 위해 준비합니다. initSegmenterTODO를 다음 구현으로 바꿉니다 (Cmd/Ctrl+f 'initSegmenter' 또는 ~62번째 줄).
    cleanup()
    try {
      withContext(singleThreadDispatcher) {
        val model =
          CompiledModel.create(
            context.assets,
            "selfie_multiclass.tflite",
            CompiledModel.Options(toAccelerator(acceleratorEnum)),
            null,
          )
        segmenter = Segmenter(model, coloredLabels)
        Log.d(TAG, "Created an image segmenter")
      }
    } catch (e: Exception) {
      Log.i(TAG, "Create LiteRT from selfie_multiclass is failed: ${e.message}")
      _error.emit(e)
    }
    

7. 세분화 및 전처리 시작

이제 모델이 있으므로 분할 프로세스를 트리거하고 모델의 입력 데이터를 준비해야 합니다.

트리거 세분화

분할 프로세스는 카메라에서 프레임을 수신하는 MainViewModel.kt에서 시작됩니다.

열기 MainViewModel.kt

  1. 카메라 프레임에서 분할 트리거: MainViewModelsegment 함수는 분할 작업의 진입점입니다. 카메라에서 새 이미지를 사용할 수 있거나 갤러리에서 이미지를 선택할 때마다 호출됩니다. 그런 다음 이러한 함수는 ImageSegmentationHelper에서 segment 메서드를 호출합니다. 두 segment 함수에서 TODO를 다음으로 바꿉니다 (107번째 줄).
    // For ImageProxy (from CameraX)
    fun segment(imageProxy: ImageProxy) {
        segmentJob =
            viewModelScope.launch {
                imageSegmentationHelper.segment(imageProxy.toBitmap(), imageProxy.imageInfo.rotationDegrees)
                imageProxy.close()
            }
    }
    
    // For Bitmaps (from gallery)
    fun segment(bitmap: Bitmap, rotationDegrees: Int) {
        segmentJob =
            viewModelScope.launch {
                val argbBitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true)
                imageSegmentationHelper.segment(argbBitmap, rotationDegrees)
            }
    }
    

이미지 전처리

이제 ImageSegmentationHelper.kt로 돌아가 이미지 전처리를 처리해 보겠습니다.

열기 ImageSegmentationHelper.kt

  1. 공개 segment 함수 구현: 이 함수는 Segmenter 클래스 내에서 비공개 segment 함수를 호출하는 래퍼 역할을 합니다. TODO를 다음으로 바꿉니다 (~95번째 줄).
    try {
      withContext(singleThreadDispatcher) {
        segmenter?.segment(bitmap, rotationDegrees)?.let { if (isActive) _segmentation.emit(it) }
      }
    } catch (e: Exception) {
      Log.i(TAG, "Image segment error occurred: ${e.message}")
      _error.emit(e)
    }
    
  2. 사전 처리 구현: Segmenter 클래스 내의 비공개 segment 함수는 모델을 위해 입력 이미지를 준비하는 데 필요한 변환을 실행하는 곳입니다. 여기에는 이미지의 크기 조정, 회전, 정규화가 포함됩니다. 그러면 이 함수가 다른 비공개 segment 함수를 호출하여 추론을 실행합니다. segment(bitmap: Bitmap, ...) 함수의 TODO을 다음으로 바꿉니다 (~121번째 줄).
    val totalStartTime = SystemClock.uptimeMillis()
    val rotation = -rotationDegrees / 90
    val (h, w) = Pair(256, 256)
    
    // Preprocessing
    val preprocessStartTime = SystemClock.uptimeMillis()
    var image = bitmap.scale(w, h, true)
    image = rot90Clockwise(image, rotation)
    val inputFloatArray = normalize(image, 127.5f, 127.5f)
    Log.d(TAG, "Preprocessing time: ${SystemClock.uptimeMillis() - preprocessStartTime} ms")
    
    // Inference
    val inferenceStartTime = SystemClock.uptimeMillis()
    val segmentResult = segment(inputFloatArray)
    Log.d(TAG, "Inference time: ${SystemClock.uptimeMillis() - inferenceStartTime} ms")
    
    Log.d(TAG, "Total segmentation time: ${SystemClock.uptimeMillis() - totalStartTime} ms")
    return SegmentationResult(segmentResult, SystemClock.uptimeMillis() - inferenceStartTime)
    

8. LiteRT를 사용한 기본 추론

입력 데이터가 사전 처리되었으므로 이제 LiteRT를 사용하여 핵심 추론을 실행할 수 있습니다.

열기 ImageSegmentationHelper.kt

  1. 모델 실행 구현: 비공개 segment(inputFloatArray: FloatArray) 함수는 LiteRT run() 메서드와 직접 상호작용하는 곳입니다. 전처리된 데이터를 입력 버퍼에 쓰고 모델을 실행하고 출력 버퍼에서 결과를 읽습니다. 이 함수의 TODO을 다음으로 바꿉니다 (~188번째 줄).
    val (h, w, c) = Triple(256, 256, 6)
    
    // MODEL EXECUTION PHASE
    val modelExecStartTime = SystemClock.uptimeMillis()
    
    // Write input data - measure time
    val bufferWriteStartTime = SystemClock.uptimeMillis()
    inputBuffers[0].writeFloat(inputFloatArray)
    val bufferWriteTime = SystemClock.uptimeMillis() - bufferWriteStartTime
    Log.d(TAG, "Buffer write time: $bufferWriteTime ms")
    
    // Optional tensor inspection
    logTensorStats("Input tensor", inputFloatArray)
    
    // Run model inference - measure time
    val modelRunStartTime = SystemClock.uptimeMillis()
    model.run(inputBuffers, outputBuffers)
    val modelRunTime = SystemClock.uptimeMillis() - modelRunStartTime
    Log.d(TAG, "Model.run() time: $modelRunTime ms")
    
    // Read output data - measure time
    val bufferReadStartTime = SystemClock.uptimeMillis()
    val outputFloatArray = outputBuffers[0].readFloat()
    val outputBuffer = FloatBuffer.wrap(outputFloatArray)
    val bufferReadTime = SystemClock.uptimeMillis() - bufferReadStartTime
    Log.d(TAG, "Buffer read time: $bufferReadTime ms")
    
    val modelExecTime = SystemClock.uptimeMillis() - modelExecStartTime
    Log.d(TAG, "Total model execution time: $modelExecTime ms")
    
    // Optional tensor inspection
    logTensorStats("Output tensor", outputFloatArray)
    
    // POSTPROCESSING PHASE
    val postprocessStartTime = SystemClock.uptimeMillis()
    
    // Process mask from model output
    val inferenceData = InferenceData(width = w, height = h, channels = c, buffer = outputBuffer)
    val mask = processImage(inferenceData)
    
    val postprocessTime = SystemClock.uptimeMillis() - postprocessStartTime
    Log.d(TAG, "Postprocessing time (mask creation): $postprocessTime ms")
    
    return Segmentation(
      listOf(Mask(mask, inferenceData.width, inferenceData.height)),
      coloredLabels,
    )
    

9. 오버레이 후처리 및 표시

추론을 실행하면 모델에서 원시 출력이 제공됩니다. 시각적 세그먼테이션 마스크를 만들고 화면에 표시하려면 이 출력을 처리해야 합니다.

열기 ImageSegmentationHelper.kt

  1. 출력 처리 구현: processImage 함수는 모델의 원시 부동 소수점 출력을 분할 마스크를 나타내는 ByteBuffer로 변환합니다. 각 픽셀에 대해 확률이 가장 높은 클래스를 찾아 이를 수행합니다. TODO을 다음으로 바꿉니다 (~238번째 줄).
    val mask = ByteBuffer.allocateDirect(inferenceData.width * inferenceData.height)
    for (i in 0 until inferenceData.height) {
        for (j in 0 until inferenceData.width) {
            val offset = inferenceData.channels * (i * inferenceData.width + j)
    
            var maxIndex = 0
            var maxValue = inferenceData.buffer.get(offset)
    
            for (index in 1 until inferenceData.channels) {
                if (inferenceData.buffer.get(offset + index) > maxValue) {
                    maxValue = inferenceData.buffer.get(offset + index)
                    maxIndex = index
                }
            }
            mask.put(i * inferenceData.width + j, maxIndex.toByte())
        }
    }
    return mask
    

열기 MainViewModel.kt

  1. 세분화 결과 수집 및 처리: 이제 MainViewModel로 돌아가 ImageSegmentationHelper의 세분화 결과를 처리합니다. segmentationUiShareFlowSegmentationResult를 수집하고 마스크를 다채로운 Bitmap로 변환하여 UI에 제공합니다. segmentationUiShareFlow 속성의 TODO를 다음으로 바꿉니다 (~63번째 줄). 이미 있는 코드는 바꾸지 말고 본문만 채우세요.
    viewModelScope.launch {
      imageSegmentationHelper.segmentation
        .filter { it.segmentation.masks.isNotEmpty() }
        .map {
          val segmentation = it.segmentation
          val mask = segmentation.masks[0]
          val maskArray = mask.data
          val width = mask.width
          val height = mask.height
          val pixelSize = width * height
          val pixels = IntArray(pixelSize)
    
          val colorLabels =
            segmentation.coloredLabels.mapIndexed { index, coloredLabel ->
              ColorLabel(index, coloredLabel.label, coloredLabel.argb)
            }
          // Set color for pixels
          for (i in 0 until pixelSize) {
            val colorLabel = colorLabels[maskArray[i].toInt()]
            val color = colorLabel.getColor()
            pixels[i] = color
          }
          // Get image info
          val overlayInfo = OverlayInfo(pixels = pixels, width = width, height = height)
    
          val inferenceTime = it.inferenceTime
          Pair(overlayInfo, inferenceTime)
        }
        .collect { flow.emit(it) }
    }
    

열기 view/SegmentationOverlay.kt

마지막 단계는 사용자가 전면 카메라로 전환할 때 세분화 오버레이를 올바르게 방향을 지정하는 것입니다. 카메라 피드는 전면 카메라에 대해 자연스럽게 미러링되므로 오버레이 Bitmap에 동일한 가로 뒤집기를 적용하여 카메라 미리보기와 올바르게 정렬되도록 해야 합니다.

  1. 오버레이 방향 처리: SegmentationOverlay.kt 파일에서 TODO를 찾아 다음 코드로 바꿉니다. 이 코드는 전면 카메라가 활성 상태인지 확인하고, 활성 상태인 경우 오버레이 BitmapCanvas에 그려지기 전에 가로로 뒤집습니다. (~42번째 줄):
    val orientedBitmap =
      if (lensFacing == CameraSelector.LENS_FACING_FRONT) {
        // Create a matrix for horizontal flipping
        val matrix = Matrix().apply { preScale(-1f, 1f) }
        Bitmap.createBitmap(image, 0, 0, image.width, image.height, matrix, false).also {
          image.recycle()
        }
      } else {
        image
      }
    

10. 최종 앱 실행 및 사용

이제 필요한 모든 코드 변경을 완료했습니다. 이제 앱을 실행하고 작업 결과를 확인해 보겠습니다.

  1. 앱 실행: Android 기기를 연결하고 Android 스튜디오 툴바에서 Run을 클릭합니다.

실행 버튼

  1. 기능 테스트: 앱이 실행되면 다채로운 세그먼테이션 오버레이가 있는 실시간 카메라 피드가 표시됩니다.
    • 카메라 전환: 상단의 카메라 전환 아이콘을 탭하여 전면 카메라와 후면 카메라 간에 전환합니다. 오버레이가 올바르게 방향을 지정하는지 확인하세요.
    • 액셀러레이터 변경: 하단의 'CPU' 또는 'GPU' 버튼을 탭하여 하드웨어 액셀러레이터를 전환합니다. 화면 하단에 표시된 추론 시간의 변화를 확인합니다. GPU가 훨씬 빨라야 합니다.
    • 갤러리 이미지 사용: 상단의 '갤러리' 탭을 탭하여 기기의 사진 갤러리에서 이미지를 선택합니다. 앱이 선택한 정적 이미지에서 세분화를 실행합니다.

기타 UI

이제 LiteRT로 구동되는 완벽하게 작동하는 실시간 이미지 세분화 앱이 있습니다.

11. 고급 (선택사항): NPU 사용

이 저장소에는 신경망 처리 장치 (NPU)에 최적화된 앱 버전도 포함되어 있습니다. NPU 버전은 호환되는 NPU가 있는 기기에서 성능을 크게 향상시킬 수 있습니다.

NPU 버전을 사용해 보려면 Android 스튜디오에서 kotlin_npu/android 프로젝트를 엽니다. 코드는 CPU/GPU 버전과 매우 유사하며 NPU 위임을 사용하도록 구성되어 있습니다.

NPU 위임을 사용하려면 사전 체험판 프로그램에 등록해야 합니다.

12. 축하합니다.

LiteRT를 사용하여 실시간 이미지 세분화를 실행하는 Android 앱을 성공적으로 빌드했습니다. 다음과 같은 내용을 배웠습니다.

  • Android 앱에 LiteRT 런타임을 통합합니다.
  • TFLite 이미지 분할 모델을 로드하고 실행합니다.
  • 모델의 입력을 사전 처리합니다.
  • 모델의 출력을 처리하여 분할 마스크를 만듭니다.
  • 실시간 카메라 앱에 CameraX를 사용합니다.

다음 단계

  • 다른 이미지 세분화 모델을 사용해 보세요.
  • 다양한 LiteRT 대리자 (CPU, GPU, NPU)로 실험합니다.

자세히 알아보기