Computer Vision & Kotlin

Обычно всяческие CV, ML, AI проекты делаются на Python или C++, но есть возможность работать с этим и из Java и, соответственно, Kotlin. Рассмотрим использование OpenCV из Kotlin на примере matchTemplate — операции поиска объекта по шаблону.

OpenCV — открытый и, наверное, самый распространённый и мощный проект по компьютерному зрению. Написан, в основном, на C/C++.

Чтобы использовать различные библиотеки C++ существует проект bytedeco.org — они предоставляют обёртки на Java, которые через JNI вызывают методы библиотек.

Вот проект — обёртка для OpenCV, который можно подключить как maven зависимость. Но, на самом деле, с этим проектом есть пара неудобств, например, прежде чем использовать его методы, нужно вызвать Loader.load(opencv_java.class) , подгрузив всю библиотеку. Это и неудобно, и занимает много времени и ресурсов, тогда как нам, бывает, нужна пара функций.

Поэтому рассмотрим проект JavaCV — это почти тоже самое, но использовать его проще и удобнее.

Итак, к делу, подключаем:

implementation("org.bytedeco:javacv-platform:1.5.2")

Теперь решим известную задачу компьютерного зрения: нахождение части изображения, наиболее совпадающую с переданным шаблоном. То есть, например, найдём вот эту

кошку среди толпы кошек:

Для начала нам нужно преобразовать полученные файлы в базовую структуру OpenCV — Mat

val image: Mat = File(args[0]).loadAsMat()
val template: Mat = File(args[1]).loadAsMat()
//...
fun File.loadAsMat(): Mat {
    val result = opencv_imgcodecs.imread(absolutePath, opencv_imgcodecs.IMREAD_COLOR)
    require(!result.empty()) { "Error loading file '$absolutePath' (Is there non-latin symbols in path?)" }
    return result
}

Mat — это матрица, одна из основных структур OpenCV, которая хранит изображение, результаты преобразований и т.п. Здесь мы средствами библиотеки читаем сразу файл в матрицу.

Далее:

val (imgReport, accuracy) = TemplateMatcher(opencv_imgproc.TM_CCOEFF_NORMED)
    .match(image, template)
opencv_imgcodecs.imwrite(args[2], imgReport)
println("Done! Accuracy: $accuracy%. Check " + args[2])

находим шаблон на изображении с помощью класса, код которого ниже, потом записываем отчёт работы в файл библиотечной функцией imwrite и пишем «готово» в консоль с указанием точности нахождения.

class TemplateMatcher(private val matchMethod: Int) {
    init {
        if (matchMethod !in arrayOf(opencv_imgproc.TM_CCORR_NORMED, opencv_imgproc.TM_CCOEFF_NORMED, opencv_imgproc.TM_SQDIFF_NORMED))
            throw RuntimeException("unknown match method $matchMethod")
    }

    fun match(img: Mat, template: Mat): Pair<Mat, Double> {
        val matchReport = Mat()
        opencv_imgproc.matchTemplate(img, template, matchReport, matchMethod)
//            opencv_core.normalize(matchReport, matchReport, 0, 1, opencv_core.NORM_MINMAX, -1, new Mat()) - если бы был matchMethod без _NORMED

        val minVal = DoublePointer(1)
        val maxVal = DoublePointer(1)
        val minLoc = Point()
        val maxLoc = Point()
        opencv_core.minMaxLoc(matchReport, minVal, maxVal, minLoc, maxLoc, null)
        val matchLoc = if (matchMethod == opencv_imgproc.TM_SQDIFF_NORMED) minLoc else maxLoc

        val result = img.clone()
        opencv_imgproc.rectangle(
            result,
            matchLoc,
            Point(matchLoc.x() + template.cols(), matchLoc.y() + template.rows()),
            Scalar(200.0, 200.0, 0.0, 0.0), 2, 8, 0
        )
        return result to getAccuracyInPercent(minVal.get(), maxVal.get())
    }

    private fun getAccuracyInPercent(minVal: Double, maxVal: Double) =
        100.0 * if (matchMethod == opencv_imgproc.TM_SQDIFF_NORMED) 1 - minVal else maxVal
}

Здесь: в блоке инициализации видим все доступные методы матчинга (есть ещё без суффикса _NORMED , но они мало чем отличаются — нужно разве что вызывать доп метод для их обработки). По личным экспериментам мне больше нравится TM_CCOEFF_NORMED

Затем видим opencv_imgproc.matchTemplate — самая важная функция, именно она находит области изображения, наиболее подходящии к шаблону.

Далее идёт анализ полученного результата: opencv_core.minMaxLoc т.к. это всё на C, то всё не так красиво, как принято на Java/Kotlin, поэтому приходится создавать вспомогательные структуры (Эта функция находит из полученных областей наиболее подходящие и оценивает точность нахождения).

Далее, с помощью opencv_imgproc.rectangle рисуем на исходном изображении квадрат, где мы нашли шаблон.

Пора запускать, найдёт ли исходного котяру?

Done! Accuracy: 99.77%.

Погружение

  • Репа с этим примером
  • matchTemplate работает достаточно прикольно — можно находить не тупо часть этого же изображения, на котором находишь, а нечто похожее. Например, на видео, где движется элемент, особо не меняя свои формы и размеры — можно его находить (хотя для отслеживания движущихся объектов используют специальные трекеры объектов — но они могут не работать, например, когда объект пропадает из кадра)
  • Примеры JavaCV
  • Актуальная (ссылка на master) статья про template matching из официальных доков OpenCV
  • Теперь, умея подключить OpenCV к проекту как зависимость, не составит труда использовать оф.документацию, переписывая их примеры на JVM языки. В оф.документации есть примеры на C++, Python и даже немного JS.
    • Правда JavaCV хоть и в чём-то удобнее, но есть небольшие отличия, так что иногда придётся искать реализацию именно на нём (например, в OpenCV для видео принято юзать VideoCapture, который в JavaCV вообще не работает, а вместо него используется FrameGrabber)
  • Книга по OpenCV (есть и другие)
  • Туториал на русском, но примеры на C++
  • Туториал для использования OpenCV из Java. Но лично мне не нравится. Там используется официальная обёртка для java, при том что для этого нужно установить OpenCV на локальный компьютер, подключить эту библиотеку как jar’ник.. Не разбирался, как это всё работает и можно ли запихнуть в репу, но в репах не нашел. Думаю, bytedeco — лучший вариант.

1 Comment

  1. Всегда страшно браться за какие-то такие темы, которые потенциально очень глубоки. А похоже всё не так уж и страшно. Классный пример с кошками!

Leave a Comment

Ваш адрес email не будет опубликован. Обязательные поля помечены *