Неделю назад мы опубликовали статью про открытый сервере для распознавания изображений автомобильных номеров. Теперь, как и обещали, статья про то, как отправлять на него свои фотографии с номерами. Наша цель была, как вы помните, вовсе не ругаться друг на друга неприличными словами, а именно сделать функционирующий сервер в интернете, который справляется с фотографиями и отправляет назад результат распознавания.
(часть фотографий, присланных в течение недели)
Хочется рассказать еще и о том, как мы — программисты, ворочающие нос от интернет технологий и 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("rn");
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()+"rn"+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%
строки разделены "rn"
Найдено 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-проекта
Update:
1) Оказывается, обученный каскад нами на российские автомобильные номера был замержин в основную версию OpenCV
2) при предварительном выделении номера ожидали картинки довольно больших размеров в uploadimage, сейчас поправил, все приводится к одному масштабу. Должно заработать и на мелких картинках из интернета.
Автор: Vasyutka