코틀린에서 opencv

aosamesan·2022년 12월 11일
0

오늘의 삽질은 코틀린에서 opencv를 써서 이미지를 가지고 노는 것입니다.

환경

  • macOS Ventura 13.0.1 (MacBook Pro 14" - M1 Pro CPU 10코어, GPU 16코어)
  • kotlin 1.7.10
  • Termurin JDK 17
  • homebrew

opencv 설치

homebrew를 이용하여 opencv를 설치할 것이다. 다만 자바나 코틀린에서 사용하기 위한 라이브러리도 만들어내야한다. 그러므로 opencv는 소스를 받아서 빌드하는 것으로 실행할 것이다. gcc가 필요하므로 다음을 실행한다.

  1. gcc 활성화
$ xcode-select --install
  1. ant 설치
    opencv를 빌드해서 사용할 것인데, jar파일을 생성할 때 ant를 사용하기 위해 ant를 먼저 설치해야한다.
    단, 현재 최신 jdk는 19인데, 19로 하면 다른거 쓰는데 좀 그래서 만만한 LTS인 11로 설치해봅시다.
    우선 $JAVA_HOME이 어떤 버전을 가리키고 있는지를 알아야한다. 이거때문에 매우 삽질을 (지금) 하는 중이다.
    jenv같은 것을 사용하면 11로 바꿔주고 source .zshrc 등을 실행하거나 터미널을 재시작하여 $JAVA_HOME이 11을 가리키고 있는지 확인한 후, 다음을 실행한다.
$ brew edit ant

열린 파일에서 "openjdk" 부분을 모두 "openjdk@11" 로 수정한다. 다음 ant를 (소스빌드로) 설치한다.

$ brew install -s ant

이후 ant -diagnostics를 실행하여 java.vm.version이 $JAVA_HOME이 가리키는 JDK와 버전이 같은지 확인하고 다음으로 넘어간다. 안 같으면 처음부터 다시 해본다.

  1. opencv 설치
    기본적으로 opencv를 빌드해서 설치할 때 jar파일을 안만들어주므로 얘도 수정해야한다. 다음을 실행한다.
$ brew edit opencv

열린 파일에서 -DBUILD_open_cv_java=OFF 부분의 OFF를 ON으로 수정한다. 다음 (소스빌드로) opencv를 설치한다.

$ brew install -s opencv

이후 /opt/homebrew/Cellar/opencv/${opencv.version}/share/java/opencv4 에 들어가보면 jar파일이 있을 것이다.
이 글을 작성할 당시 설치한 버전은 4.6.0_1 이다.

gradle 디펜던시로 추가

디펜던시에 다음과 같이 추가한다.

implementation(files("{위에서 확인한 jar 파일 경로}"))

이제 opencv를 쓸 수 있다! 라고 생각할 수 있으나 한가지 더 설정해야한다.
위에까지만 설정하고 그냥 쓰려고하면 에러가 발생하므로 네이티브 라이브러리 path를 설정해주어야한다.

지금 Multiplatform Compose 데스크톱으로 테스트 하는 중인데 이걸 먼가 공통적으로 실행할 수 없나 보는데 안되네
메인에 다음 코드를 추가한다.

System.setProperty("java.library.path", "/opt/homebrew/Cellar/opencv/4.6.0_1/share/java/opencv4")
System.loadLibrary(Core.NATIVE_LIBRARY_NAME)

테스트의 경우에는 gradle 설정에 다음을 추가한다.

tasks.withType<Test> {
	systemProperty("java.library.path", "${systemProperties["java.library.path"]}:/opt/homebrew/Cellar/opencv/4.6.0_1/share/java/opencv4")
}

이제 잘 된다.

사실 어제 갑자기 생각나서 하려고 보니 보통 wrapper들만 있고 게다가 유지보수가 안되는 것 같아서 직접 설치하는 걸로 했는데... 일단 보니 JNI 쓰는 거 같아서 먼가 어렵다. 자바 예제 코드를 찾아보니 별로 없고 opencv만 쳐봐도 거의 대부분이 파이썬 코드라 뭘 쓰려면 일단 파이썬 코드를 참고하고, OpenCV Java DOC에서 메서드가 어디에 있는건지 확인하고 사용하면 된다.

File to Mat

이미지를 불러와서 행렬로 저장하기 위해서는 다음과 같이 한다.

val image = Imgcodecs.imread("/path/to/image")

BufferedImage to Mat

java.awt.image.BufferedImage를 Mat으로 바꿀 수 있다.
다만 읽어들인 이미지의 컬러 채널에 따라서 매우 복잡하다. (삽질하고옴)

fun bufferedImage2Mat(image: BufferedImage): Mat {
  val dataBuffer = image.raster.dataBuffer
  val channel = dataBuffer.size / (image.height * image.width)
  val cvType = when (dataBuffer.dataType) {
      DataBuffer.TYPE_BYTE -> CvType.CV_8UC(channel)
      DataBuffer.TYPE_SHORT -> CvType.CV_16SC(channel)
      DataBuffer.TYPE_USHORT -> CvType.CV_16UC(channel)
      DataBuffer.TYPE_INT -> CvType.CV_32SC(channel)
      DataBuffer.TYPE_FLOAT -> CvType.CV_32FC(channel)
      else -> throw IllegalArgumentException()
  }
  val mat = Mat(image.height, image.width, cvType)
  if (dataBuffer is DataBufferByte) {
      originalMat.put(0, 0, dataBuffer.data)
  } else if (dataBuffer is DataBufferShort) {
      originalMat.put(0, 0, dataBuffer.data)
  } else if (dataBuffer is DataBufferUShort) {
      originalMat.put(0, 0, dataBuffer.data)
  } else if (dataBuffer is DataBufferInt) {
      originalMat.put(0, 0, dataBuffer.data)
  } else if (dataBuffer is DataBufferFloat) {
      originalMat.put(0, 0, dataBuffer.data)
  } else {
      throw IllegalArgumentException()
  }
  return mat
}

Mat to BufferedImage

얘는 간단하다.

fun mat2BufferedImage(mat: Mat): BufferedImage {
    return ImageIO.read(ByteArrayInputStream(MatOfByte(mat).toArray()))
}

Grayscale

색이 있는 이미지를 그레이스케일로 바꿔봅시다.

fun toGrayScale(image: BufferedImage): BufferedImage {
    val original = bufferedImage2Mat(image)
    val grayscale = MatOfByte()
    Imgproc.cvtColor(original, grayscale, Imgproc.COLOR_RGB2GRAY)
    Imgcodecs.imencode(".png", grayscale, grayscale)
    return mat2BufferedImage(grayscale)
}

보면 grayscale을 imencode해서 내보내는데, cvtColor를 이용하면 source(original)과 같은 RGB색이 다 있는 것을 사용하다가 gray값만 있는 이미지로 바뀌었으므로 바로 이미지로 전환할 수 없어서 한 번 인코딩을 하고 내보내야한다.
imencode의 첫 파라미터는 파일 확장자인데 꼭 .을 붙여야한다. 귀찮으니 png로 고정

그래서 간단한 프로그램

@Composable
@Preview
fun FrameWindowScope.App() {
    var image by mutableStateOf<BufferedImage?>(null)
    var imageExt by mutableStateOf("")
    var converted by mutableStateOf<BufferedImage?>(null)
    val parent = this.window

    MaterialTheme {
        Row(modifier = Modifier.fillMaxSize().padding(3.dp)) {
            Column(modifier = Modifier.fillMaxHeight().fillMaxWidth(.5f).padding(1.dp, 0.dp)) {
                Row(modifier = Modifier.fillMaxWidth().weight(1f)) {
                    if (image != null) {
                        Image(image!!.toPainter(), "Original", contentScale = ContentScale.Fit)
                    }
                }
                Row(modifier = Modifier.wrapContentHeight().fillMaxWidth()) {
                    Button(onClick = {
                        val dlg = FileDialog(parent).apply {
                            this.setFilenameFilter { dir, name ->
                                listOf(".jpg", ".png").map { name.endsWith(it, true) }
                                    .fold(false) { acc, b -> acc || b }
                            }
                            this.isVisible = true
                            this.isMultipleMode = false
                        }
                        if (dlg.directory != null && dlg.file != null) {
                            val filePath = "${dlg.directory}/${dlg.file}"
                            val file = File(filePath)
                            imageExt = file.extension
                            image = ImageIO.read(file)
                        }
                    }, modifier = Modifier.fillMaxWidth()) {
                        Text("Open Image")
                    }
                }
            }
            Column(modifier = Modifier.fillMaxHeight().padding(1.dp, 0.dp)) {
                Row(modifier = Modifier.fillMaxWidth().weight(1f)) {
                    if (converted != null) {
                        Image(converted!!.toPainter(), "Converted", contentScale = ContentScale.Fit)
                    }
                }
                Row(modifier = Modifier.wrapContentHeight().fillMaxWidth()) {
                    Button(
                        onClick = {
                            if (image != null) {
                                converted = toGrayScale(image!!)
                            }
                        },
                        modifier = Modifier.fillMaxWidth(),
                        enabled = if (image !== null) {
                            image!!.colorModel.numColorComponents > 1
                        } else {
                            false
                        }
                    ) {
                        Text("Grayscale")
                    }
                }
            }

        }
    }
}

fun main() {
    System.setProperty("java.library.path", "/opt/homebrew/Cellar/opencv/4.6.0_1/share/java/opencv4")
    System.loadLibrary(Core.NATIVE_LIBRARY_NAME)
    application {
        Window(onCloseRequest = ::exitApplication) {
            App()
        }
    }
}

실행시키면 다음과 같이 나온다.

아무 이미지나 열어서 Grayscale 버튼을 누르면 바꿔준다.

profile
재미로 개발 하는 사람

0개의 댓글