Распознавание номеров: от А до 9. Часть 3

от автора

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


(часть фотографий, присланных в течении недели)

Хочется рассказать еще и о том, как мы — программисты, ворочающие нос от интернет технологий и Linux, — решали проблему с сервером.
Все мысли по поводу настоящего шумного компьютера под ухом, протягивание кабеля на кухню и переговоров с провайдером про реальный IP, были отброшены, как не соответствующие новым реалиям (со всех сторон только и говорят про облачные сервисы и прочие новинки). Но еще хотелось удобства, привычного Windows, dotNET, да и вообще возможности по-живому отлаживаться на сервере. Посему было решено: виртуальный сервер с Windows Server и удаленный рабочий стол.
Хочу передать огромное спасибо терпеливым и вежливым парням в техподдержке! Так что справились.


Да-да, вот так все просто выглядит. Это принтскрин с удаленного доступа к виртуальному серверу (да не сочтите это рекламой Windows Server 2012 R2).

Затем надо было написать http ответчик. Хотелось как можно проще и не связываться с IIS, нужно было уложиться в пару дней на разработку. Но оказалось очень просто скачать пример SimpleHttpServer и в функцию:

public override void handlePOSTRequest(HttpProcessor p, StreamReader inputData) {         Console.WriteLine("POST request: {0}", p.http_url);         string data = inputData.ReadToEnd();                  p.outputStream.WriteLine("<html><body><h1>test server</h1>");         p.outputStream.WriteLine("<a href=/test>return</a><p>");         p.outputStream.WriteLine("postbody: <pre>{0}</pre>", data);     }

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

Доступ к серверу

Сервер распознавания работает, как очень простой http сайт. Пользователь отправляет на страницу post-сообщение в формате http, в котором содержится лишь один параметр — изображение. В ответ получает результат распознавания.
Для запроса из БД, если в этом есть необходимость, нужно отправить 2 строки: автомобильный номер в текстовом виде и уникальный ID.
В Android программе было 3 запроса, их код выглядит следующим образом:

1) отправка предварительно выделенного номера серверу:

HttpClient httpclient = new DefaultHttpClient(); final HttpParams httpParameters = httpclient.getParams(); HttpConnectionParams.setConnectionTimeout(httpParameters, 10 * 1000); HttpConnectionParams.setSoTimeout        (httpParameters, 10 * 1000); //Создаём Http запрос и прилагаем к нему файл изображения HttpPost httppost = new HttpPost("http://193.138.232.71:10000/result"); InputStreamEntity reqEntity; httppost.setEntity(new FileEntity(new File(FileName), "application/octet-stream")); //Получаем ответ от сервера     try { 		HttpResponse response = httpclient.execute(httppost); 		HttpEntity responseEntity = response.getEntity(); 		ans = EntityUtils.toString(responseEntity); 		String[] strs=ans.split("\r\n"); 		if(strs.length>2) 		{ 	         	ans=strs[0]; //Получаемый от сервера распознанный номер         	        timesWas=Integer.parseInt(strs[1]); //Сколько раз он встречался в базе                 	ID=strs[2]; //Унакальный ID текущей операции 		} 	} catch (ClientProtocolException e) { 			e.printStackTrace(); 			ans = "NOT CONNECT"; 	} catch (IOException e) { 	        e.printStackTrace(); 		ans = "NOT CONNECT"; 	} 

2) отправка запроса по номеру:

HttpClient httpclient = new DefaultHttpClient(); final HttpParams httpParameters = httpclient.getParams(); HttpConnectionParams.setConnectionTimeout(httpParameters, 10 * 1000); HttpConnectionParams.setSoTimeout        (httpParameters, 10 * 1000);       HttpPost httppost = new HttpPost("http://193.138.232.71:10000/checkplate"); InputStreamEntity reqEntity;   try { 	 httppost.setEntity(new StringEntity( editText1.getText().toString()+"\r\n"+ID));     	 HttpResponse resp = httpclient.execute(httppost); 	 HttpEntity ent = resp.getEntity(); 	String ans = EntityUtils.toString(ent); 	timesWas=Integer.parseInt(ans); 	textView.setText("Уже обозвали раз: "+Integer.toString(timesWas));          	} catch (ClientProtocolException e) { 		e.printStackTrace(); 	} catch (IOException e) { 		e.printStackTrace(); 	}catch(Exception e) 	{ 		e.printStackTrace(); 	} 

3) «ругань» на номер:

HttpClient httpclient = new DefaultHttpClient(); final HttpParams httpParameters = httpclient.getParams(); HttpConnectionParams.setConnectionTimeout(httpParameters, 10 * 1000); HttpConnectionParams.setSoTimeout        (httpParameters, 10 * 1000); HttpPost httppost = new HttpPost("http://193.138.232.71:10000/swear"); InputStreamEntity reqEntity;             try {         	 httppost.setEntity(new StringEntity( editText1.getText().toString()));             	 HttpResponse resp = httpclient.execute(httppost);         	 HttpEntity ent = resp.getEntity();         	String ans = EntityUtils.toString(ent); 		textView.setText("Обозван");             } catch (ClientProtocolException e) { 		e.printStackTrace();			             } catch (IOException e) { 		e.printStackTrace();	 	    }catch(Exception e) 			{ 				e.printStackTrace(); 			} 

По-моему, комментировать тут особенно нечего. HttpPost файла и HttpPost двух текстовых строк.

Не забывайте, что в условиях использования мобильного интернета, приходится отправлять область с предварительно обнаружнным номером с помощью каскадного детектора Хаара.
Пример кода выделения Хааром с помощью OpenCV на Android Java:

//Детектирование каскадом Хаара номера if (mJavaDetector != null) mJavaDetector.detectMultiScale(temp, faces, 1.1, 10, 5,  	                		 new Size(70, 21), new Size(500,150)); //Если нашлось	 Rect[] facesArray = faces.toArray(); for (int i = 0; i < facesArray.length; i++) {  	DetectedNum = new Mat(); 	IsNumDetected=true;  	//Новая рамка с чуть большими границами  	int dW=facesArra[i].width/5; // расширяем рамку по X на 20% 	int dH=facesArray[i].height*3/10; //по Y на 30%    	int left = Math.max(facesArray[i].x-dW/2,0);        	int top = Math.max(facesArray[i].y-dH/2,0); 	int right = facesArray[i].x+facesArray[i].width+dW/2; if(right>temp.width())right=temp.width()-1;         int bottom = facesArray[i].y+facesArray[i].height+dH/2; if(bottom>temp.height())bottom=temp.height()-1;  	//Отправка на сервер данного куска 	DetectedNum = temp.submat(BiggerRect).clone();   } 

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

И по просьбе трудящихся добавили http заход на функцию поиска и распознавания номера в целом кадре: 193.138.232.71:10000/uploadimage
В ответ получите список найденных номеров и некий критерий качества распознавания по каждому (больше — лучше):
x000xx99 90%
a111aa197 75%
строки разделены "\r\n"
Найдено 2 номера, первый более качественный (90%), второй менее (75%).

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

На других платформах код должен получаться не намного сложнее.

Несколько слов о трех днях полета сервера распознавания номеров

Программу на Android для ругани на автомобили Recognitor мы выложили 13 мая. У меня чувства смешанные: от гордости от того, что оно работает, до сжигающего стыда за случающиеся ошибки в алгоритме распознавания, когда прямо на глазах приходит чистый четкий номер, но пользователю возвращается абракадабра.

Количество отправленных на сервер изображений: 1700
Из них оказалось номерами РФ: 1370
Количество распознанных: 830
(с точностью до 10ти указано)

Вот тут стоит отдельно пояснить «из них оказалось номерами РФ». Мы не учли, что хабр хорошо читают на территории СНГ и нигде не указали, что номера должны быть РФ. Естественно, сюда же относятся и ошибки не идеально обученного каскадного детектора, который часто ошибался в непривычной ситуации съемки с монитора. И было несколько десятков зеркально отраженных номеров, т. е. пользователь не выбрал в меню “Flip”. Также ну очень сильно размазанные (не читаемые глазами) я тоже отнес сюда.
В промежуточном итоге результат не фантастический, мы сделали выводы, уже выпустили 2 обновления Android программы, поправив косяки и дав пользователю новую волшебную функцию выделения области номера пальцем. Изменили алгоритмы на сервере. О том, что интересного мы поменяли в самих алгоритмах, в моей следующей статье (воспользовались парой альтернативных методов из предыдущей моей статьи).
Но, не смотря на не идеальную работу, пользователям приложение пришлось по душе! Оценки в GooglePlay радовали.

И да, конечно, поощрим бесспорных победителей:
P494KE_197 — обозван 226 раз (конечно, это ZlodeiBaal)
X777XX_77 – обозван 21 раз (в топе запроса яндекса на запрос «номера»)
Даже поймали A362MP_97, А231МР_97 и А869МР_97 (возможно, тоже из интернета).

Удачи

Вообще, алгоритм обучался на очень грязных зимних номерах (и парадоксально не всегда устойчиво работает на чистых), поэтому тут то его преимущества и стоит поискать. И да, действительно, часто размытые и весьма грязные номера удавалось распознать:


Ссылки:
Часть 1
Часть 2
Обновленные исходники Android-проекта

ссылка на оригинал статьи http://habrahabr.ru/post/223441/


Комментарии

Добавить комментарий

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