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() 메서드를 실행하면 된다.
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는 화면이 처음 시작할 때 크기를 알 수 있다. 로그는 이후 설명을 위해 잠깐 추가하였다.
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 미 적용
setContent {
AndroidView(modifier = Modifier.fillMaxSize(), factory = { openCVCameraView })
}
Load, Inference라는 인터페이스를 아래와 같이 만든다.
Inference에서는 추론에 관련된 내용을 담을 것이고, Load에서는 모델을 로드하는 내용을 담을 것이다.
interface Inference : Load {
}
class MainActivity : ComponentActivity(), CameraBridgeViewBase.CvCameraViewListener2, Inference {
// 이전과 동일
...
}
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 객체가 추론에 사용될 객체이다.
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 값에 맞는 라벨링 값들을 화면에 적용할 것이다.
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/ 에서 모델을 넣어보면 확인할 수 있다.
우리는 디폴트 모델을 사용하고 있지만, 개별적으로 학습한 모델을 사용하고 싶으면 위의 데이터에 맞게 수정하면 된다.
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 객체에 추론된 결과값이 저장되게 된다. 이후 이 결과값을 후처리 과정을 지나 화면에 표출하면 된다.
override fun onCameraFrame(inputFrame: CameraBridgeViewBase.CvCameraViewFrame?): Mat {
val frameMat = inputFrame!!.rgba()
detect(frameMat, net, labels)
return frameMat
}
체크 포인트를 설정하고 디버깅을 해보면 아래 output과 같이 잘 나오는 것을 확인할 수 있다.
다음 글부터 후처리에 대한 내용이 될 것이다.