nagoHok
-
Количество публикаций
54 -
Зарегистрирован
-
Посещение
-
Days Won
1
Сообщения, опубликованные пользователем nagoHok
-
-
Друзья, а у Вас есть какая-либо выборка состоящая из изображений авто с номерными знаками? хотяб пару сотен обучающих изображений?
я этими пользуюсь
- 1
-
Оч сырой код, но более 50% локализует, предлагаю довести его до ума)
#include "stdafx.h" #include "ContrastMap.h" #pragma warning (disable:4996) #include <cv.h> #include <opencv2\highgui\highgui_c.h> #include <math.h> #pragma comment(lib,"opencv_core220d.lib") #pragma comment(lib,"opencv_highgui220d.lib") #pragma comment(lib,"opencv_imgproc220d.lib") #pragma comment(lib,"opencv_video220d.lib") #pragma comment(lib,"opencv_legacy220d.lib") #ifdef _DEBUG #define new DEBUG_NEW #endif // The one and only application object CWinApp theApp; using namespace std; class CBlock { public: int m_nStartPoin; int m_nEndPoint; int m_nBlockLen; CBlock(void) { m_nStartPoin = 0; m_nEndPoint = 0; m_nBlockLen = 0; } virtual ~CBlock(void) { } void Empty() { m_nStartPoin = 0; m_nEndPoint = 0; m_nBlockLen = 0; } bool IsNull(){return m_nBlockLen ? false : true;} }; void SummRow(const IplImage* pSrc, CArray<double>& arRow) { arRow.RemoveAll(); arRow.SetSize(pSrc->height); for (int nPosY = 0; nPosY < pSrc->height; nPosY++) { int nOffset = pSrc->widthStep * nPosY; for (int nPosX = 0; nPosX < pSrc->widthStep; nPosX++) { arRow[nPosY] += (uchar)pSrc->imageData[nOffset + nPosX]; } } } void DrawGraf(IplImage* pSrc, const CArray<double>& arRow) { CvPoint ptLastPoint = cvPoint(0, 0); for (int nPos = 0; nPos < arRow.GetCount(); nPos++) { cvDrawLine(pSrc, ptLastPoint, cvPoint((int)arRow[nPos] / 200, nPos) , cvScalar(0), 1); ptLastPoint = cvPoint((int)arRow[nPos] / 200, nPos); } } void AplyFilter(const IplImage* pSrc, IplImage* pDest) { double arMask[9] = {0,0,0,0,0.5,0,0,0,0}; //double arSharp[9] = {0.1111, -0.8889, 0.1111, -0.8889, 4.1111, -0.8889, 0.1111, -0.8889, 0.1111}; CvMat matMask = cvMat(3, 3, CV_64FC1, arMask); cvFilter2D(pSrc, pDest, &matMask, cvPoint(-1,-1)); } void AplySobel(const IplImage* pScr, IplImage* pDest) { IplImage* pDst = cvCreateImage( cvSize(pScr->width, pScr->height), IPL_DEPTH_16S, pScr->nChannels); // применяем оператор Собеля cvSobel(pScr, pDst, 1, 0, 3); // преобразуем изображение к 8-битному cvConvertScale(pDst, pDest); cvReleaseImage(&pDst); } inline double GSmooth(double nDist, double nParam, double nParam2) { return exp((-(nDist * nDist)/nParam)) / nParam2; } void GauseSmooth(const CArray<double>& arSrc, CArray<double>& arDest) { arDest.RemoveAll(); double nParam = 3.; int nAperture = 5; double nParam1 = 2* (nParam * nParam); double nParam2 = sqrt(2 * CV_PI) * nParam; arDest.SetSize(arSrc.GetCount()); for (int nPos = 0; nPos < arSrc.GetCount() - 1; nPos++) { double s = 0.; for (int nK = max(nPos - nAperture, 0); nK < min(arSrc.GetCount() - 1, nPos + nAperture); nK++) { s += arSrc[nK] * GSmooth(abs(nPos - nK), nParam1, nParam2); } arDest.SetAt(nPos, s); } } void Func(IplImage* pImg, int nPosY, CArray<double>& arFunc) { arFunc.RemoveAll(); double _f = 0.; int nLinePos = nPosY * pImg->widthStep; arFunc.SetSize( pImg->widthStep - 1); for(int nPos = 0; nPos < pImg->widthStep - 1; nPos++) { _f += abs(pImg->imageData[nLinePos + nPos + 1] - pImg->imageData[nLinePos + nPos]); arFunc.SetAt(nPos, _f); } } void GetFirstDiff(const CArray<double>& arSrc, CArray<double>& arDest) { CArray<double> arTmp; arTmp.SetSize(arSrc.GetCount()); for (int nPos = 1; nPos < arSrc.GetCount(); nPos++) { if(nPos + 1 > arSrc.GetCount() - 1) continue; double ndY = arSrc[nPos + 1] - arSrc[nPos]; arTmp.SetAt(nPos, ndY); } arDest.RemoveAll(); arDest.Copy(arTmp); arTmp.RemoveAll(); } int GetMaxPosition(CArray<double>& arGraf) { int nMax = 0; for (int nPos = 0; nPos < arGraf.GetCount(); nPos++) { if(arGraf[nPos] > arGraf[nMax]) nMax = nPos; } return nMax; } void GetLocalMinMax(const CArray<double>& arData, CArray<int>& arPositionsMin, CArray<int>& arPositionsMax) { int nCount = 0; double nMin = 0, nMax = 0; bool bIsMaxF = false; bool bIsMinF = false; for(int nPos = 1; nPos < arData.GetCount() - 1; nPos++) { //Отслеживаем уменьшение. if ((nPos == 1) || (arData[nPos - 1] < arData[nPos])) { nMax = arData[nPos]; bIsMaxF = true; } if ((nPos == 1) || (arData[nPos - 1] > arData[nPos])) { nMin = arData[nPos]; bIsMinF = true; } //Если ранее было обнаружено уменьшение. if(bIsMaxF) { //Отслеживаем увеличение. if ((nPos == arData.GetCount() - 1) || (arData[nPos - 1] > arData[nPos])) { //Очередной локальный max найден. arPositionsMax.Add(nPos); nCount++; if(nCount == 1) nMin = nMax; else if(nMax < nMin) nMin = nMax; bIsMaxF = false; } } if(bIsMinF) { //Отслеживаем увеличение. if ((nPos == arData.GetCount() - 1) || (arData[nPos - 1] < arData[nPos])) { //Очередной локальный минимум найден. arPositionsMin.Add(nPos); nCount++; if(nCount == 1) nMax = nMin; else if(nMin > nMax) nMax = nMin; bIsMinF = false; } } } } void CalcBlocks(int nMinBlockLen, CArray<CBlock>& m_arWidthBlock, double nAvVal, const CArray<double>& arData) { m_arWidthBlock.RemoveAll(); CBlock block; int nSpaces = 0; for (int nPos = 0; nPos < arData.GetCount(); nPos++) { if(arData[nPos] >= nAvVal) { if(block.m_nStartPoin) { block.m_nBlockLen++; nSpaces = 0; } else { block.m_nStartPoin = nPos; block.m_nBlockLen++; nSpaces = 0; } } else { if(block.m_nStartPoin) { if(nSpaces >= nMinBlockLen) { block.m_nEndPoint = nPos - nSpaces; block.m_nBlockLen -= nSpaces; nSpaces = 0; m_arWidthBlock.Add(block); block.Empty(); continue; } nSpaces++; } } } if(block.m_nStartPoin) { block.m_nEndPoint = arData.GetCount() - nSpaces; m_arWidthBlock.Add(block); } } int _tmain(int argc, TCHAR* argv[], TCHAR* envp[]) { IplImage* pFrame = NULL; //Захваченый фрейм IplImage* pGray = NULL; //Фрейм приведенный к серому цвету IplImage* pGrath = NULL; //Тут рисуем графики CFileFind finder; //BOOL bWorking = finder.FindFile(_T("c:/Utils/Работа/Мое/Распознание/Саша/Day/*.*")); BOOL bWorking = finder.FindFile(_T("C:/Users/Я/Desktop/Avto-Control Demo/Images/Day/*.*")); while (bWorking) { bWorking = finder.FindNextFile(); if(!bWorking) break; CStringA strFileName(finder.GetFilePath()); if(finder.IsDots()) continue; TRACE(strFileName.GetString()); TRACE(_T("\n")); pFrame = cvLoadImage(strFileName); if(!pFrame) continue; DWORD dwStart = GetTickCount(); cvSmooth(pFrame, pFrame, CV_GAUSSIAN, 3, 3); //Создаем серое изображение pGray = cvCreateImage(cvGetSize(pFrame), 8, 1); cvConvertImage(pFrame, pGray, CV_BGR2GRAY); //Выравниваем яркость изображения cvEqualizeHist(pGray, pGray); CArray<double> arGraf; AplySobel(pGray, pGray); SummRow(pGray, arGraf); int nMaxPos = GetMaxPosition(arGraf); CArray<double> arFunc; Func(pGray, nMaxPos, arFunc); CArray<double> arSmooth; //GauseSmooth(arFunc, arSmooth); CArray<double> arDif; GetFirstDiff(arFunc, arDif); CArray<int> arMins; CArray<int> arMaxs; GetLocalMinMax(arDif, arMins, arMaxs); int nMaxFromMax = GetMaxPosition(arDif); double nAvValMax = arDif[nMaxFromMax] * 0.2; CArray<CBlock> arBlocks; CalcBlocks(20, arBlocks, nAvValMax, arDif); /* for (int nPos = 0; nPos < arMaxs.GetCount(); nPos++) { nAvValMax += arDif[arMaxs[nPos]]; }*/ //nAvValMax = nAvValMax / arMaxs.GetCount(); //nAvValMax = nAvValMax + nAvValMax * 0.2; pGrath = cvCreateImage(cvGetSize(pFrame), 8, 1); DrawGraf(pFrame, arGraf); CvPoint ptLast = cvPoint(0, nMaxPos); int nCount = 0; // for (int nPos = 0; nPos < arDif.GetCount(); nPos++) // { // //if(arDif[nPos] < nAvValMax) // //{ // // //cvDrawLine(pGrath, cvPoint(nPos, nMaxPos), cvPoint(nPos, nMaxPos - (int)(arDif[nPos])), cvScalar(0), 1); // // continue; // //} // // nCount++; // cvDrawLine(pFrame, cvPoint(nPos, nMaxPos), cvPoint(nPos, nMaxPos + (int)(arDif[nPos] / 5)), cvScalar(0), 1); // ///* // int nPosY = (nMaxPos + (int)arDif[nPos] / 5); // cvDrawLine(pGrath, ptLast, cvPoint(nPos, nPosY), cvScalar(0), 1); // ptLast = cvPoint(nPos, nPosY); //*/ // } for (int nPos = 0; nPos < arBlocks.GetCount(); nPos++) { CBlock &block = arBlocks[nPos]; int nHigh = (int)(block.m_nEndPoint - block.m_nStartPoin) / 9.2; if(arBlocks[nPos].m_nBlockLen < 10 || arBlocks[nPos].m_nBlockLen > 100) continue; cvDrawRect(pFrame, cvPoint(arBlocks[nPos].m_nStartPoin - 10, nMaxPos - nHigh), cvPoint(arBlocks[nPos].m_nEndPoint + 10, nMaxPos + nHigh), cvScalar(255), 2); } std::cout << nCount << " " << GetTickCount() - dwStart << std::endl ; cvNamedWindow("Graf"); cvShowImage("Graf", pFrame); cvReleaseImage(&pGray); cvReleaseImage(&pFrame); cvReleaseImage(&pGrath); char c = cvWaitKey(0); if (c == 27) { break; } } return 0; }
- 1
-
Отделить надо и это уже делается. Сейчас GDI в Windows оставлен только для совместимости. Я не уверен, что в Windows 8 полностью от него не откажутся. Он в какой-то степени удобен, но уже устарел.
Мы же сейчас говорим о быстром выводе видео и всевозможных примитивов? Тогда GDI для этого не применяется уже давно: все плейеры его не используют. Он уже ушёл из этой области.
Если говорить о пользовательском интерфейсе, то и тут GDI сдал свои позиции Aero (а это полигоны + текстуры). На Линуксах, как я уже писал постом выше, тоже давно отказываются от двумерной графики для пользовательских интерфейсов, обрабатываемой CPU, а выводят её с помощью OpenGL. Хочешь ещё примеры? Браузеры (!!!) используют аппаратное ускорение для рендеринга страниц.
Если говорить о непосредственной обработке и выводе графики на монитор, тогда конечно ГДИ - это динозавр, но что бы распечатать, то теже самые Директы и ОпенДЖэли будут пользовать ГДИ.
Мы об одном и томже только с разных сторон
-
P.S. Думаю, что GDI надо оставить прошлому. А Брезенхама (Брезенхема или Брезенхейма) мы всё равно никогда не забудем.
От ГДИ не уйти, это унификация всего вывода графики.
Тут надо отделить мух от котлет, для чего нужен ГДИ и для чего нужны Директы и ОпенДЖеэли
Если первый предоставляет простой механизм для вывода графической информации неважно на какое либо устройство, то вторые это мощные средства для обработки аудио и видио
кратко как то так
-
Автор, ну так что у Вас получилось можете показать пример и исходник?
пока все в процессе
Сразу попутный вопрос, можно ли произвести фильтрацию изначального изображения КИХ фильтром?
-
-
помоему было изначально понятно, что метод с таким же успехом найдет и фару вместо номерного знака.
тут можно вводить только ограничения на высоту, ширину номерного знака и сделать такую же гистограмму не только по горизонтали, но и вертикали.
хмм, я тут подумал что будет лучше сделать гистограмму для каждого отдельного "блока" вертикальной гистограммы.
-
Так попробовал срезать значение гистограммы с помощью среднего значения и дисперсии, в принципе результат неплохой, сейчас подошел не посредственно к локализации пластины, что можете посоветовать?
Прикладываю 3 картинки, в принципе на изображениях непосредственно от 3 до 5 областей где может находиться номерная пластина, как же выбрать нужную область?
Спасибо за рание.
-
Можно ещё сгладить гистограмму и найти все значения, которые удовлетворяют условию: x[i-1] < x > x[i+1]
Но с дисперсией лучше.
P.S. Всем: а вообще, MeanShift не для этого предназначен?
MeanShift? а можно про него по конкретнее?
-
Можно попробовать найти среднее и дисперсию, и уровень отсечения установить, скажем, на 2 сигма от среднего.
Или установить минимальное количество локальных минимумов и пройти по гистограмме с некоторым шагом пока уровень отсечения не будет пересекать гистограмму заданное кол-во раз. Можно еще смотреть ширину пересечений.
ну среднее - я понял что такое, а вот дисперсия? По точнее можно узнать как её готовить?
-
это локальные максимумы.
найти глобальный максимум, и выделить их задав уровень, например если > 90% от максимума и отстоит от максимума на какое то расcтояние dist, то локальный максимум.
невсегда бывает корректно, порой глобальный максимум многократно превышает локальные, хотя для начала можно развивать и эту идею.
-
это локальные максимумы.
найти глобальный максимум, и выделить их задав уровень, например если > 90% от максимума и отстоит от максимума на какое то расcтояние dist, то локальный максимум.
а глобальный максимум это я так понимаю наибольший всплеск на гистограмме?
-
-
так, вернулся о5 таки к проекту, удалось сгладить полученую функци, но быстродействие оставляет желать лучшего, с тем учотом что еше надо и локализовать область номерного знака а затем попытаться распознать символы
если кому интересна вот реализации сглаживающей функции
inline double GausSmooth(double nDist, double nParam, double nParam2) { return exp((-(nDist * nDist)/nParam)) / nParam2; } void SmoothFunc1(const CArray<_Line>& arSrc, CArray<_Line>& arDest) { arDest.RemoveAll(); double nParam = 15.; int nAperture = 50; double nParam1 = 2* (nParam * nParam); double nParam2 = sqrt(2 * CV_PI) * nParam; for(int nPosY = 0; nPosY < arSrc.GetCount() - 1; nPosY++) { _Line line; line = arSrc[nPosY]; for (int nPosX = 0; nPosX < arSrc[nPosY].GetSize() - 1; nPosX++) { double s = 0.; for (int nK = max(nPosX - nAperture, 0); nK < min(arSrc[nPosY].GetSize() - 1, nPosX + nAperture); nK++) { s += arSrc[nPosY].Get(nK) * GausSmooth(abs(nPosX - nK), nParam1, nParam2); } line.SetAt(nPosX, s); } arDest.Add(line); } }
- 1
-
я изначально конвертирую изображение к градации серого
ЗЫ я не спрашиваю как, меня интересует надо ли это делать?
-
так тут походу разбора полетов возник вопрос
интенсивность яркости у нас от 0 до 255, значения IplImage::imageData у нас char т.е. от -127 до 128, правильно ли будет для получения функции приобразовывать char к unsigned char? или это надуманно?
-
-
ЗЫ: 5 точек, маловато, я думаю надо примерно от четверти до половины номера в длину брать.
так а как определить длинну номера тогда?
К томуже если при движение размеры плитки будут меняться?
-
Только int-ы на float или double поменяйте, иначе ничего гладкого не получится.
С целочисленной арифметикой надо очень аккуратно работать иначе очень долго можно потом ошибку искать.
Вот результат того же алгоритма но с вещественными значениями, имхо с интами было лучше)
ЗЫ вывод надо менять алгоритм сглаживания.
-
Нашел ошибку в реализации
void GetFirstDiff(const CArray<_Line>& arSrc, CArray<_Line>& arDest) { arDest.RemoveAll(); for (int nPosY = 0; nPosY < arSrc.GetCount(); nPosY++) { _Line line; line.SetSize(arSrc[nPosY].GetSize()); line.m_nYPosition = arSrc[nPosY].m_nYPosition; for (int nPosX = 0; nPosX < arSrc[nPosY].GetSize() - 2; nPosX++) { int nDiff = 0; int nY = arSrc[nPosY].m_nYPosition; if(nPosX + 1 > arSrc[nPosY].GetSize()) return; int ndY = (nY - arSrc[nPosY].m_arData[nPosX + 1]) - (nY - arSrc[nPosY].m_arData[nPosX]); int ndX = nPosX + 1 - nPosX; //Можно и не делить if(ndX) //Так как шаг всегда 1 nDiff = ndY / ndX; line.m_arData[nPosX] = nDiff; } arDest.Add(line); } }
да сейчас придется поработать над сглаживанием
-
значит ошибка где то в коде, т.к. график у вас получился правильный и там видно, что 2 участка где ф-ия почти не возрастает (по бокам), и "лесенка" (в середине).
попробуйте еще шаг поменять, т.е. шаг =step то, y2=F(i+step) y1=F(i) и x2-x1=step, хотя не думаю, что это поможет.
по большому счету вы уже сейчас можете определить границы таблички.
взять какой то отступ(в % или константой), и пройтись от начала т.е. до точки 0+отступ (это будет начало таблички) и от конца до точки конец-отступ (это будет конец таблички).
вообще метод кажется не особо надежным, обычно делают детектор движения(определяют движущийся объект-машину)+ Канни(или что то похожее) + поиск прямоугольника(таблички).
да сейчас как раз эксперементирую, на счет надежности метода - это часть алгоритма локализации таблички, конечноже она будет будет работать совместно с детектором движения, хотя пока не стоит забегать в перед.
-
ну вот вы получили функцию F(x).
если брать шаг =1 то, y2=F(i+1) y1=F(i) и x2-x1=1
втом то и дело, что я реализовал, но график производной - прямая
т.е. разница приращения y2-y1 всего 1 пиксель
-
ну производную, то в дискретном случае можно взять как (y2-y1)/(x2-x1).
как сглаживать и зачем не знаю.
Поясните пжл, что в данном случае y2 y1, x2 x1?
Спасибо за рание!
Утро вечера мудренее
void GetFirstDiff(const CArray<_Line>& arSrc, CArray<_Line>& arDest) { arDest.RemoveAll(); for (int nPosY = 0; nPosY < arSrc.GetCount(); nPosY++) { _Line line; line.SetSize(arSrc[nPosY].GetSize()); line.m_nYPosition = arSrc[nPosY].m_nYPosition; for (int nPosX = 0; nPosX < arSrc[nPosY].GetSize() - 2; nPosX++) { int nDiff = 0; int nY = arSrc[nPosY].m_nYPosition; if(nPosX + 1 > arSrc[nPosY].GetSize()) return; int ndY = (nY - arSrc[nPosY].m_arData[nPosX + 1]) - (nY - arSrc[nPosY].m_arData[nPosX]); int ndX = arSrc[nPosY].m_arData[nPosX + 1] - arSrc[nPosY].m_arData[nPosX]; if(ndX) nDiff = ndY / ndX; line.m_arData[nPosX] = nDiff; } arDest.Add(line); } }
правда получается сплошная линия, толи я опять что то не так сделал, толи флрмула производной не та.
-
так ну вроде получилось заставить работать функцию, спасибо Smorodov, сейчас осталось взять от неё производные, и работать с локализацией пластины.
Можно еще поработать со сглаживанием, но изначальная оптимизация ЗЛО! Хотя и в данном варианте быстродействие меня вполне устраивает.
class _Line { int m_nSize; public: _Line() :m_nSize(0) ,m_nYPosition(0) {}; virtual ~_Line(){}; CArray<int> m_arData; int GetSize() const {return m_nSize;}; void SetSize(int nSize) { if(nSize <= 0) return; m_nSize = nSize; m_arData.SetSize(m_nSize, 10); } _Line& operator = (const _Line& src) { SetSize(src.m_nSize); m_nYPosition = src.m_nYPosition; m_arData.Copy(src.m_arData); return *this; } int m_nYPosition; }; void SmoothFunc(const CArray<_Line>& arSrc, CArray<_Line>& arDest) { arDest.RemoveAll(); for (int nPosY = 0; nPosY < arSrc.GetCount(); nPosY++) { _Line line; line.SetSize(arSrc[nPosY].GetSize()); line.m_nYPosition = arSrc[nPosY].m_nYPosition; for (int nPosX = 0; nPosX < arSrc[nPosY].GetSize() - 2; nPosX++) { int nSmooth = 0; for (int nSmoothPos = -2; nSmoothPos < 3; nSmoothPos++) { if(nPosX < 2 && nSmoothPos < 0) continue; nSmooth += arSrc[nPosY].m_arData[nPosX + nSmoothPos]; } nSmooth /= 5; line.m_arData[nPosX] = nSmooth; } arDest.Add(line); } } void Func(IplImage* pImg, int nPosY, _Line& line) { unsigned int _f = 0; int nLinePos = nPosY * pImg->widthStep; line.SetSize( pImg->widthStep - 1); for(int nPos = 0; nPos < pImg->widthStep - 1; nPos++) { _f += abs(pImg->imageData[nLinePos + nPos + 1] - pImg->imageData[nLinePos + nPos]); line.m_arData[nPos] = _f; } } int _tmain(int argc, TCHAR* argv[], TCHAR* envp[]) { cvNamedWindow("Org"); cvNamedWindow("Gray"); IplImage* frame = NULL; frame = cvLoadImage("c:/test.jpg"); IplImage* gray = NULL; gray = cvCreateImage(cvGetSize(frame), 8, 1); cvConvertImage(frame, gray, CV_BGR2GRAY); CvPoint ptLast = cvPoint(0, 0); DWORD dwStart = GetTickCount(); int nLastYPos = 0; CArray<_Line> arLines; CArray<_Line> arLinesSmoth; for (int nY = 0; nY < gray->height; nY += 10) { //ptLast = cvPoint(0, nY); _Line line; line.m_nYPosition = nY; Func(gray, nY, line); arLines.Add(line); } SmoothFunc(arLines, arLinesSmoth); CArray<_Line> arDraw; arDraw.Copy(arLinesSmoth); for (int nPosY = 0; nPosY < arDraw.GetCount(); nPosY++) { if(nPosY != 46) //Убрать!!!! continue; //Убрать!!!! ptLast = cvPoint(0, arDraw[nPosY].m_nYPosition); for (int nPosX = 0; nPosX < arDraw[nPosY].GetSize(); nPosX++) { int f = arDraw[nPosY].m_arData[nPosX] / 50; cvLine(frame, ptLast, cvPoint(nPosX, arDraw[nPosY].m_nYPosition - f) , cvScalar(0), 1); ptLast = cvPoint(nPosX, arDraw[nPosY].m_nYPosition - f); } } dwStart = GetTickCount() - dwStart; std::cout << dwStart; cvShowImage("Gray", gray); cvShowImage("Org", frame); cvReleaseImage(&gray); cvReleaseImage(&frame); gray = NULL; frame = NULL; char c = cvWaitKey(0); if (c == 27) { return 0; } return 0; }
Вывод видео на форму в visual studio 2008 - 2010
в OpenCV
Опубликовано · Report reply
Не советую использовать для этого таймер, ибо в карте сообщений системы он стоит самый последний, лучше для этого организовать отдельный поток.