YOLOv8 segmentation 안드로이드 -3

알로에·2023년 10월 11일
1

YOLOv8-Segmentation

목록 보기
3/6

✔ 1. 카메라 추가

  1. 이전글에서 카메라 권한을 manifest에 추가하였다. 이제 사용자에게 카메라 권한 요청을 해야한다.
private fun setPermissions() {
        val requestPermissionLauncher =
            registerForActivityResult(ActivityResultContracts.RequestPermission()) {
                if (!it) {
                    Toast.makeText(this, "권한을 허용 하지 않으면 사용할 수 없습니다!", Toast.LENGTH_SHORT).show()
                    finish()
                }
            }

        val permissions = listOf(Manifest.permission.CAMERA)

        permissions.forEach {
            if (ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED) {
                requestPermissionLauncher.launch(it)
            }
        }
        OpenCVLoader.initDebug()
    }

카메라 권한 하나만 있는 경우는 list로 할당할 필요가 없지만, 추후 추가될 권한이 있는 경우를 대비해서 list로 할당하였다.
마지막으로 OpenCV 라이브러리를 사용할 수 있게 initDebug() 메서드를 실행하면 된다.

  1. 화면 처리를 위해 카메라 리스너를 구현해야 한다.
    MainActivity에 CameraBridgeViewBase.CvCameraViewListener2를 implement 한다. 그 후 차례대로 아래 메서드들을 오버라이딩 한다.
class MainActivity : ComponentActivity(), CameraBridgeViewBase.CvCameraViewListener2 {
// 이전과 동일
...

 override fun onCameraViewStarted(width: Int, height: Int) {
        Log.d("onCameraViewStarted", "onCameraViewStarted: $width, $height")
    }

    override fun onCameraViewStopped() {
    }

    override fun onCameraFrame(inputFrame: CameraBridgeViewBase.CvCameraViewFrame?): Mat {
        return inputFrame!!.rgba()
    }
}

onCameraFrame에서 화면 처리를 담당하게 된다.
onCameraViewStarted는 화면이 처음 시작할 때 크기를 알 수 있다. 로그는 이후 설명을 위해 잠깐 추가하였다.

  1. 카메라로 부터 받은 화면에 대한 View 생성
    onCreate 메서드에 openCVCameraView 객체를 생성한다.
val openCVCameraView = ((JavaCameraView(this, CAMERA_ID)) as CameraBridgeViewBase).apply {
            setCameraPermissionGranted()
            enableView()
            setCvCameraViewListener(this@MainActivity)
            setMaxFrameSize(640, 640)
            layoutParams = ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT
            )
        }

여기서 CAMERA_ID는 사전에 companion object에 추가해야 한다.

companion object {
        // 전면 = 1, 후면 = 0
        const val CAMERA_ID = 0
    }

여기서 setMaxFrameSize은 화면의 가로 크기와 세로 크기의 최대값을 제한하는 것이다. 여기서 중요한 점은 최대값을 제한하는 것이지 화면의 가로와 세로 크기가 640으로 고정되는 것이 아니다. 위에서 로그로 확인한 것을 보면

Log.d("onCameraViewStarted", "onCameraViewStarted: $width, $height")

onCameraViewStarted: 640, 480 으로 로그가 나오게 된다.
OpenCV 에서 제공하는 크기 중 640,640으로 제한했을 때 나올 수 있는 사이즈로 알맞게 적용되서 나오게 된다.

또한 layoutParams으로 전체 화면의 크기로 설정해야 한다. 그렇지 않으면 frame size에 맞게 화면이 줄어들게 된다.
아래 사진을 통해 간단히 layoutParams를 주석 처리 했을 때와 차이점을 알 수 있다.

layoutParams 적용

layoutParams 미 적용

  1. View 적용
    아래 코드를 통해 간단히 View를 Compose를 이용해서 적용할 수 있다.
setContent {
            AndroidView(modifier = Modifier.fillMaxSize(), factory = { openCVCameraView })
        }

✔ 2. 모델 로드

Load, Inference라는 인터페이스를 아래와 같이 만든다.

Inference에서는 추론에 관련된 내용을 담을 것이고, Load에서는 모델을 로드하는 내용을 담을 것이다.

  1. Inference 인터페이스를 Load를 구현하게 설정한다.
interface Inference : Load {

}
  1. MainActivity가 Inference를 구현하게 설정한다.
class MainActivity : ComponentActivity(), CameraBridgeViewBase.CvCameraViewListener2, Inference {

// 이전과 동일
...

}
  1. Load 인터페이스에 모델과 라벨링 값들을 가져오는 메서드를 추가한다.
interface Load {

    companion object {
        const val FILE_NAME = "yolov8n-seg.onnx"
        const val LABEL_NAME = "yolov8n.txt"
    }

    fun loadModel(assets: AssetManager, fileDir: String): Net {
        val outputFile = File("$fileDir/$FILE_NAME")
        assets.open(FILE_NAME).use { inputStream ->
            FileOutputStream(outputFile).use { outputStream ->
                val buffer = ByteArray(1024)
                var read: Int
                while (inputStream.read(buffer).also { read = it } != -1) {
                    outputStream.write(buffer, 0, read)
                }
            }
        }
        return Dnn.readNetFromONNX("$fileDir/$FILE_NAME")
    }

    fun loadLabel(assets: AssetManager): Array<String> {
        BufferedReader(InputStreamReader(assets.open(LABEL_NAME))).use { reader ->
            var line: String?
            val classList = mutableListOf<String>()
            while (reader.readLine().also { line = it } != null) {
                line?.let { l -> classList.add(l) }
            }
            return classList.toTypedArray()
        }
    }

}

companion object 안에는 각각 모델명과 라벨링 파일명을 추가한다.
OpenCV에서는 Dnn.readNetFromONNX 메서드를 통해 Onnx 모델을 불러올 수 있다. 반환하는 Net 객체가 추론에 사용될 객체이다.

  1. MainActivity 에서 해당 메서드들을 실행한다.
private lateinit var net: Net
private lateinit var labels: Array<String>

override fun onCreate(savedInstanceState: Bundle?) {

// 이전과 동일 
...
net = loadModel(assets, filesDir.toString())
labels = loadLabel(assets)

}

이후 이 net 객체를 이용하여 추론하고, labels 값을 통해 index 값에 맞는 라벨링 값들을 화면에 적용할 것이다.

✔ 3. 추론

  1. Inference 인터페이스에 companion object을 추가한다.
companion object {

        const val OUTPUT_NAME_0 = "output0"
        const val OUTPUT_NAME_1 = "output1"

        const val INPUT_SIZE = 640
        const val SCALE_FACTOR = 1 / 255.0

        const val OUTPUT_SIZE = 8400
        const val OUTPUT_MASK_SIZE = 160

        const val CONFIDENCE_THRESHOLD = 0.3f
        const val NMS_THRESHOLD = 0.5f
    }

output의 이름, 각종 크기들은 https://netron.app/ 에서 모델을 넣어보면 확인할 수 있다.

우리는 디폴트 모델을 사용하고 있지만, 개별적으로 학습한 모델을 사용하고 싶으면 위의 데이터에 맞게 수정하면 된다.

  1. detect 메서드를 정의한다.
    fun detect(mat: Mat, net: Net, labels: Array<String>) {
    
        val inputMat = Mat()
        Imgproc.resize(mat, inputMat, Size(INPUT_SIZE.toDouble(), INPUT_SIZE.toDouble()))
        Imgproc.cvtColor(inputMat, inputMat, Imgproc.COLOR_RGBA2RGB)
        inputMat.convertTo(inputMat, CvType.CV_32FC3)
        val blob = Dnn.blobFromImage(inputMat, SCALE_FACTOR)
        net.setInput(blob)

        val output0 = Mat()
        val output1 = Mat()
        val outputList = arrayListOf(output0, output1)
        val outputNameList = arrayListOf(OUTPUT_NAME_0, OUTPUT_NAME_1)

        net.forward(outputList, outputNameList)

        blob.release()
        inputMat.release()

    }

forward 메서드가 실행되면 outputList 안에 Mat 객체에 추론된 결과값이 저장되게 된다. 이후 이 결과값을 후처리 과정을 지나 화면에 표출하면 된다.

  1. detect 메서드 적용
    이전에 설명했듯이, onCameraFrame에서 화면 처리 메서드를 적용하면 된다.
override fun onCameraFrame(inputFrame: CameraBridgeViewBase.CvCameraViewFrame?): Mat {
        val frameMat = inputFrame!!.rgba()
        detect(frameMat, net, labels)
 
        return frameMat
    }


체크 포인트를 설정하고 디버깅을 해보면 아래 output과 같이 잘 나오는 것을 확인할 수 있다.

다음 글부터 후처리에 대한 내용이 될 것이다.

0개의 댓글

관련 채용 정보