오늘의 삽질은 코틀린에서 opencv를 써서 이미지를 가지고 노는 것입니다.
homebrew를 이용하여 opencv를 설치할 것이다. 다만 자바나 코틀린에서 사용하기 위한 라이브러리도 만들어내야한다. 그러므로 opencv는 소스를 받아서 빌드하는 것으로 실행할 것이다. gcc가 필요하므로 다음을 실행한다.
$ xcode-select --install
source .zshrc
등을 실행하거나 터미널을 재시작하여 $JAVA_HOME이 11을 가리키고 있는지 확인한 후, 다음을 실행한다.$ brew edit ant
열린 파일에서 "openjdk"
부분을 모두 "openjdk@11"
로 수정한다. 다음 ant를 (소스빌드로) 설치한다.
$ brew install -s ant
이후 ant -diagnostics
를 실행하여 java.vm.version
이 $JAVA_HOME이 가리키는 JDK와 버전이 같은지 확인하고 다음으로 넘어간다. 안 같으면 처음부터 다시 해본다.
$ 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
이다.
디펜던시에 다음과 같이 추가한다.
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에서 메서드가 어디에 있는건지 확인하고 사용하면 된다.
이미지를 불러와서 행렬로 저장하기 위해서는 다음과 같이 한다.
val image = Imgcodecs.imread("/path/to/image")
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
}
얘는 간단하다.
fun mat2BufferedImage(mat: Mat): BufferedImage {
return ImageIO.read(ByteArrayInputStream(MatOfByte(mat).toArray()))
}
색이 있는 이미지를 그레이스케일로 바꿔봅시다.
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 버튼을 누르면 바꿔준다.
끗