Skip to main content

Распознавание номеров в MatLab. Первая часть. Статика, прямые предикаты.

В этой статье я попробую кратко описать процесс распознавания автомобильных регистрационных номеров.
result
Это задание было дано на курсе «Введение в компьютерное зрение 2010» Лаборатории Компьютерной Графики и Мультимедиа ВМК МГУ.
Задание было поделено на три этапа:

  1. Распознавание основных 3 цифр номера с картинки, содержащей только номерной знак (ну и чутка области вокруг). Изображение не нормированы, номера находятся примерно горизонтально;
  2. Распознавание всех символов с номера (суть задания была в том, чтобы обучить и применить классификатор, сама реализация классификатора была нам дана);
  3. Распознавание номера из видео потока ( несколько секунд видео, содержащего одну проезжающую машину);

Процесс.

Ко всем задачам нам были даны тестовые и обучающие выборки, реализация Random Forest.
Вести свой рассказ я буду в том порядке, в котором выполнял эти задания.

Задание 1. Статика

Пример исходной картинки
716.bmp

Кратко план задания можно описать так:

  1. Сделать предобработку изображения (убрать шум, исправить освещенность и тд.);
  2. Выделить области интереса (найти положения символов, которые нужно распознать);
  3. Рассчитать исследуемые параметры областей интереса ( Область интереса — некоторый блоб на картинке);
  4. Сделать вывод об объекте;
function digits = CarNumberRecognition(filename) %Функция принимает на вход имя файла с картинкой и отдает массив чисел
srcI = imread(filename); %считаем исходник
srcImod = srcI; %и запишем его в конвеер обработки (чтобы сохранить на будущее)

Предобработка.

Как вы можете видеть на примере сверху, изображение очень зашумлено. Плохая контрастность, разнородная освещенность, шум — все это мешает в лоб применять механизмы обнаружения объектов.
Первый шаг.
imadjust делает линейное растяжение гистограммы изображения. При отсутствии параметров он растягивает ее так, чтобы минимальная и максимальная яркости пикселей были 0 и 255 соответственно.

srcImod = imadjust(srcImod);

adjusted image
Уже лучше.
Второй шаг.
Нас не устраивает мелкий мусор на картинке — шум камеры. От него мы избавимся медианной фильтрацией.

srcImod = medfilt2(srcImod,[3 3]);

filtered image
Около цифр исчезли точки на белом фоне. Теперь можно искать линии.
Третий шаг.
Для поиска линий воспользуемся фильтром «Laplacian of Gaussian» или log. Если критична скорость — его работу можно заменить более быстьым фильтром DoG (difference of Gaussian). Он быстрее, но чуть-чуть менее качественный
Статья в Wiki о LoG. Кратко — ЛоГ — детектор блобов заданного размера. Параметры размеров задаются эмпирически (более подробно о связи сигмы фильтра и размера блоба в вики).

filt = fspecial('log',[7 7], 0.3);
srcImod = imfilter(srcImod,filt);

laplacian image
Четвертый шаг.
Теперь бинаризуем наше изображение, отделив фон от объекта. Хвала матлабу, это записывается одной приятной формулой:

srcImod = srcImod(:,:,:) < 150;

bin image
Зачем мне темные пиксели, воскликните вы? Ведь все цифры выделены белым?
Да белым. Но тут я использую маленькую хитрость для отсеивания цифро-подобного мусора, не входящего в номер. Сначала я попытаюсь найти номерной знак. А в нем произвести повторный поиск.
Как вы можете заметить, рамка номера — один большой блоб с большим периметром, немаленькой площадью (по сравнению с шумом и цифрами) и очень маленькой компактностью (отношением площади к периметру). Для того чтобы протестировать все найденные блобы, мы используем встроенный алгоритм разметки бинарного изображения.
Но перед этим мы немного попытаемся улучшить себе жизнь, замкнув область рамки номера ( если она сольется с фоном, то цифры мы будем искать таки в целой картинке, а не только в номере). Сделаем мы это с помощью операции морфологического раскрытия, с маской в виде диска радиуса 2:

srcImod = imopen(srcImod,strel('disk',2));
srcImod = (bwlabel(srcImod,8));

Результат раскрытия:
open image
Вот наши сегменты картинки (раскраска делается командой label2rgb(srcImod), но нам это не нужно ):
label2rgb image

Поиск областей интереса.

Переберем все сегменты, в которых будем искать цифры.
Поиск самого номера
Команда regionprops вычисляет заданные характеристики областей, на которые размечена картинка. В данном случае мне понадобились поля «Площадь», «Изображение», «Ориентация», «Рамка», «Полная площадь (включая внутренности)». Критерий, по которому я ищу номер, — это некоторая эвристика, рассчитанная ( и частями подобранная, что греха таить) исходя из геометрических размеров номера.
ratio — отношение ширины к высоте номера, Convex Area — полная площадь объекта, включая дырки. Если номер мне подходит, я забиваю в специальный массив картинку-содержимое этого номера из исходного изображения.

Idata = regionprops(srcImod,'Area','Image','Orientation','BoundingBox','ConvexArea');
k = 1;
for i=1:length(Idata)
    ratio = Idata(i).BoundingBox(3)/Idata(i).BoundingBox(4);
    if Idata(i).ConvexArea > 500 && ratio < 7 && ratio > 2.5
        bound = floor(Idata(i).BoundingBox);
        numbers(k) = Idata(i);
        numbers(k).Image = imcrop(srcI,bound);
        k = k +1;
    end
end

Далее в цикле по всем найденным платам мы будем искать что-нибудь, похожее на цифры.

count = k - 1;
k = 1;
for i=1:count

Поиск цифр.

Поиск цифр в целом похож на поиск номера. Сначала мы делаем предобработку изображения. Мы вырезали область номера из исходного изображения, для того чтобы сделать повторную обработку только этой области. Это поможет нам избежать влияния стороннего шума, освещения и прочего, не принадлежащего данной области

srcImod = imrotate(numbers(i).Image, -numbers(i).Orientation,'bicubic','crop');
srcImod = imadjust(srcImod);
filt = fspecial('log',[7 7], 0.32);
srcIlog = imfilter(srcImod,filt);
srcImod = srcIlog;
srcImod = imresize(srcImod,[400 NaN]);
srcImod = srcImod (:,:,:) > 150;
sl = strel('disk',7);
srcImod = imopen(srcImod,sl);
sl = strel('disk',8);
srcImod = imclose(srcImod,sl);

То же самое — adjust, LoG, бинаризация (теперь уже по >150), морфологическое закрытие для удаления мелкого мусора, морфологическое раскрытие для устранения незамкнутости. Область увеличивается чтобы упростить эвристику по площади.

Расчет параметров и вывод.

for i=1:length(probdigits)
ratio = probdigits(i).BoundingBox(3)/probdigits(i).BoundingBox(4);
if probdigits(i).Area > 2200 && ratio < 0.7 && ratio > 0.1
ImageToTest = imresize(probdigits(i).Image,[64 64]);
c = zeros();
for j=1:10,  c(j)= corr2 (ImageToTest,idealnum(j).Image);  end
c = abs(c);
maxid = 1;
for j=2:10, if c(j) > c(maxid), maxid = j; end, end
if c(maxid) > 0.55
digits(k) = (maxid-1);
digareas(k).bound = PlateBounds + probdigits(i).BoundingBox;
k = k + 1;
end
if k==4, break, end
end
end

В этом месте объясню по-подробней. Для каждого блоба мы рассчитываем его удлиненность, площадь. Если область достаточно большая ( Area > 2200 ), и имеет определенную удлиненность ( ratio < 0.7 && ratio > 0.1 ), то мы вычисляем коэффициент корреляции этой области с каждой «идеальной» цифрой. Эти цифры сгенерированы заранее. Дальше мы выбираем наилучшее совпадение, и, если оно больше некоторого порога — принимаем это за хороший результат. Как только мы набираем три цифры подряд — обсчет заканчивается. Регионы перебираются слева направо, так что нужные нам цифры выйдут в нужном порядке.
result
Здесь реализован самый простой алгоритм вывода цифры по параметрам — сравнение с шаблоном. Есть несколько вариантов, у каждого есть свои плюсы и минусы:
Рассчитать для блоба такие параметры, как удлиненность, центр масс, компактность, число Эйлера и другие. Например, если число Эйлера равно 2, то это точно 8, если 1 — это 6,9,0. Если 0 — все остальное. 1,3 и 7 имеют характерно низкую компактность. 6 и 9 различаются центрами масс. Используя такие знания можно построить простые и понятные предикаты, которые по набору параметров выдадут результат. Правда этот метод неустойчив к шуму и искажению контура ( например разрывам колец )
Можно обучить классификатор, который по некоторым параметрам блоба ( в принципе туда можно запихнуть хоть всю матрицу картинки) будет выдавать результат. Это не так просто, но дает более качественные результаты, метод устойчив к искажениям. Про машинное обучение я подробнее расскажу во второй части.
Ну и метод который реализован — вполне просто, приемлемая надежность, приемлемая устойчивость.

Заключение

Данный код, как и статья не претендует на академичность, я пытался лишь в простой форме рассказать основы поиска объектов. Алгоритм можно улучшать в нескольких направлениях. Например, можно учесть информацию о порядке цифр и искать только те блобы, которые походят под более мягкие критерии, но идут подряд и имеют примерно одинаковую высоту.

Код и ссылки:

http://static.scaytrase.ru/cnr/cnr1.zip — Архив первого задания, в том виде, в котором я его сдал на проверку ( точность около 70% полностью распознанных номеров и около 80% распознанных цифр)
http://static.scaytrase.ru/cnr/cnr2.zip — Архив второго задания
http://static.scaytrase.ru/cnr/CV2010.zip — Полный архив исходных кодов, включая тестовую и обучающую выборку по всем трем заданиям. Код может содержать отладочные returnы, так что тестить лучше отправленный код.
http://courses.graphicon.ru/ — сайт курсов лаборатории КГиММ ВМК МГУ. Много интересных материалов по теме.
http://cgm.computergraphics.ru/ — сетевой журнал о КГиММ. Тоже много хороших статей.
http://static.scaytrase.ru/cnr/article-cnr-eng.pdf — статья о распознавании номеров на вагонах (немного другой подход к сегментированию )

http://static.scaytrase.ru/cnr/RandomForest.zip — Выданный нам классификатор на МатЛаб
http://static.scaytrase.ru/cnr/RF_tutorial.doc — Мануал по использованию RandomForest.