Задача

Пусть есть такая картинка:

original

Хочется разделить картинку на много маленьких картинок: одна буква - одна картинка, и тогда дальше можно будет воспользоваться HoG классификатором с прошлого урока:

boxes

Документация

На протяжении всего задания предлагается воспользоваться несколькими готовыми функциями из OpenCV, и чтобы с ними было проще разобраться - предлагается использовать документацию.

Вбейте в гугл opencv docs cpp.

Убеждаетесь что сверху слева указана версия библиотеки навроде 4.5.4 (т.к. мы используем версию 4.5.1 или выше - см. файл в корне проекта CMakeLists.txt):

opencv docs version

Например представим что мы хотим найти документацию по функции cv::threshold(...):

opencv docs search

Теперь с помощью переводчика можно разобраться что делает эта функция:

opencv docs threshold function example

Когда у функции много аргументов (параметров) - легко забыть какой из них вы сейчас хотите указать в среде разработаки (IDE CLion), поэтому удобно поставив каретку внутри вызываемой функции попросить IDE подсказать список названий параметров - для этого нажмите CTRL+P (P - от слова Parameters).

Бинаризация картинки

Давайте сначала сделаем картинку черно-белой.

1) Теперь давайте все почти идеально белые пиксели сделаем окончательно белыми (фон), а почти идеально черные буквы сделаем идеально черными. Это то же самое что и “сравним каждый пиксель с некоторым порогом (например с 127, т.к. полный диапазон яркости от 0 до 255) и все что меньше - занулим, а все что больше - увеличим до 255”:

Этого можно добиться как раз с помощью этой функции cv::threshold(...) (прочитайте про нее документацию):

binary thresholding bad

Заметьте что из-за градиента (плавного перепада яркости картинки слева направо) - такой порог выбрать не выйдет:

  • либо мы потеряем буквы слева (т.к. они будут приняты как фон - станут белыми)

  • либо мы потеряем буквы справа (т.к. вместе с ними и фон будет принят за буквы - станет черным и сольется с буквами)

2) Давайте заметим что нам как людям тем не менее очевидно где буквы находятся. И ясно нам это за счет локального контраста. Т.е. порог должен быть не глобальным, а локальным. Буква должна просто быть достаточно сильно отличима от фона.

Как раз с этим может помощь функция cv::adaptiveThreshold(...) (прочитайте про нее документацию):

binary thresholding bad

Склеиваем буквы

3) Чтобы буквы не были такими шумными и рваными (иногда даже разваливающимися на кусочки - см. например букву a или цифру 4) - давайте попробуем выполнить морфологические операции - расширение или же сужение, т.е. dilate или же erode.

Прочитайте документацию про:

  • cv::getStructuringElement - по сути это то какие пиксели из окрестности будут учитываться при выполнении морфологического “правда ли что рядом есть хотя бы один белый пиксель?”, иначе говоря нас устроит прямоугольник

  • cv::dilate

  • cv::erode

Например для cv::erode получаем что-то вроде:

erode results

Выглядит неплохо! А какой результат у cv::dilate? А как он меняется при изменении радиуса приложения структурного элемента (т.е. при изменении окрестности по которой мы проверяем правда ли что рядом кто-то есть)?

4) Подумайте какая из этих двух картинок нам дальше больше подходит?

Выделяем компоненты (связанные пиксели)

5) Выделяем контура - очертания букв, для этого подойдет функция cv::findContours (прочитайте документацию!).

Визуализируем результат

6) Чтобы посмотреть как нашлись буквы - давайте нарисуем вокруг каждой из них - прямоугольник (bounding box).

Бонус

100) Если вы со всем справились - возьмите теперь этот подход разбивания на отдельные буквы, затем приклейте к этому результат прошлого урока - распознавание каждой буквы.