Надежное обнаружение карт / последовательная коррекция OpenCV

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

Вот код, который я использую, чтобы выполнить эту операцию:

    Mat img = inImg.clone();
outImg = Mat(inImg.size(), CV_8UC1);
inImg.copyTo(outImg);

Mat img_fullRes = img.clone();

pyrDown(img, img);

Mat imgGray;
cvtColor(img, imgGray, CV_RGB2GRAY);
outImg_gray = imgGray.clone();
// Find Edges //

Mat detectedEdges = imgGray.clone();

bilateralFilter(imgGray, detectedEdges, 0, 185, 3, 0);
Canny( detectedEdges, detectedEdges, 20, 65, 3 );

dilate(detectedEdges, detectedEdges, Mat::ones(3,3,CV_8UC1));
Mat cdst = img.clone();

vector<Vec4i> lines;
HoughLinesP(detectedEdges, lines, 1, CV_PI/180, 60, 50, 3 );
for( size_t i = 0; i < lines.size(); i++ )
{
Vec4i l = lines[i];
// For debug
//line( cdst, cv::Point(l[0], l[1]), cv::Point(l[2], l[3]), Scalar(0,0,255), 1);
}
//cdst.copyTo(inImg);

//    // Find points of intersection //

cv::Rect imgROI;
int ext = 10;
imgROI.x = ext;
imgROI.y = ext;
imgROI.width = img.size().width - ext;
imgROI.height = img.size().height - ext;

int N = lines.size();
// Creating N amount of points // N == lines.size()
cv::Point** poi = new cv::Point*[N];
for( int i = 0; i < N; i++ )
poi[i] = new cv::Point[N];
vector<cv::Point> poiList;
for( int i = 0; i < N; i++ )
{
poi[i][i] = cv::Point(-1,-1);
Vec4i line1 = lines[i];
for( int j = i + 1; j < N; j++ )
{
Vec4i line2 = lines[j];
cv::Point p = computeIntersect(line1, line2, imgROI);

if( p.x != -1 )
{
//line(cdst, p-cv::Point(2,0), p+cv::Point(2,0), Scalar(0,255,0));
//line(cdst, p-cv::Point(0,2), p+cv::Point(0,2), Scalar(0,255,0));

poiList.push_back(p);
}

poi[i][j] = p;
poi[j][i] = p;
}
}

cdst.copyTo(inImg);

if(poiList.size()==0)
{
outImg = inImg.clone();
//circle(outImg, cv::Point(100,100), 50, Scalar(255,0,0), -1);
return;
}

convexHull(poiList, poiList, false, true);

for( int i=0; i<poiList.size(); i++ )
{
cv::Point p = poiList[i];
//circle(cdst, p, 3, Scalar(255,0,0), 2);
}
//Evaluate all possible quadrilaterals

cv::Point cardCorners[4];
float metric_max = 0;
int Npoi = poiList.size();
for( int p1=0; p1<Npoi; p1++ )
{
cv::Point pts[4];
pts[0] = poiList[p1];

for( int p2=p1+1; p2<Npoi; p2++ )
{
pts[1] = poiList[p2];
if( isCloseBy(pts[1],pts[0]) )
continue;

for( int p3=p2+1; p3<Npoi; p3++ )
{
pts[2] = poiList[p3];
if( isCloseBy(pts[2],pts[1]) || isCloseBy(pts[2],pts[0]) )
continue;for( int p4=p3+1; p4<Npoi; p4++ )
{
pts[3] = poiList[p4];
if( isCloseBy(pts[3],pts[0]) || isCloseBy(pts[3],pts[1])
|| isCloseBy(pts[3],pts[2]) )
continue;// get the metrics
float area = getArea(pts);

cv::Point a = pts[0]-pts[1];
cv::Point b = pts[1]-pts[2];
cv::Point c = pts[2]-pts[3];
cv::Point d = pts[3]-pts[0];
float oppLenDiff = abs(a.dot(a)-c.dot(c)) + abs(b.dot(b)-d.dot(d));

float metric = area - 0.35*oppLenDiff;
if( metric > metric_max )
{
metric_max = metric;
cardCorners[0] = pts[0];
cardCorners[1] = pts[1];
cardCorners[2] = pts[2];
cardCorners[3] = pts[3];
}

}
}
}
}

// find the corners corresponding to the 4 corners of the physical card
sortPointsClockwise(cardCorners);

// Calculate Homography //

vector<Point2f> srcPts(4);
srcPts[0] = cardCorners[0]*2;
srcPts[1] = cardCorners[1]*2;
srcPts[2] = cardCorners[2]*2;
srcPts[3] = cardCorners[3]*2;vector<Point2f> dstPts(4);
cv::Size outImgSize(1400,800);

dstPts[0] = Point2f(0,0);
dstPts[1] = Point2f(outImgSize.width-1,0);
dstPts[2] = Point2f(outImgSize.width-1,outImgSize.height-1);
dstPts[3] = Point2f(0,outImgSize.height-1);

Mat Homography = findHomography(srcPts, dstPts);

// Apply Homography
warpPerspective( img_fullRes, outImg, Homography, outImgSize, INTER_CUBIC );
outImg.copyTo(inImg);

куда computeIntersect определяется как:

cv::Point computeIntersect(cv::Vec4i a, cv::Vec4i b, cv::Rect ROI)
{
int x1 = a[0], y1 = a[1], x2 = a[2], y2 = a[3];
int x3 = b[0], y3 = b[1], x4 = b[2], y4 = b[3];

cv::Point  p1 = cv::Point (x1,y1);
cv::Point  p2 = cv::Point (x2,y2);
cv::Point  p3 = cv::Point (x3,y3);
cv::Point  p4 = cv::Point (x4,y4);
// Check to make sure all points are within the image boundrys, if not reject them.
if( !ROI.contains(p1) || !ROI.contains(p2)
|| !ROI.contains(p3) || !ROI.contains(p4) )
return cv::Point (-1,-1);

cv::Point  vec1 = p1-p2;
cv::Point  vec2 = p3-p4;

float vec1_norm2 = vec1.x*vec1.x + vec1.y*vec1.y;
float vec2_norm2 = vec2.x*vec2.x + vec2.y*vec2.y;
float cosTheta = (vec1.dot(vec2))/sqrt(vec1_norm2*vec2_norm2);

float den = ((float)(x1-x2) * (y3-y4)) - ((y1-y2) * (x3-x4));
if(den != 0)
{
cv::Point2f pt;
pt.x = ((x1*y2 - y1*x2) * (x3-x4) - (x1-x2) * (x3*y4 - y3*x4)) / den;
pt.y = ((x1*y2 - y1*x2) * (y3-y4) - (y1-y2) * (x3*y4 - y3*x4)) / den;

if( !ROI.contains(pt) )
return cv::Point (-1,-1);

// no-confidence metric
float d1 = MIN( dist2(p1,pt), dist2(p2,pt) )/vec1_norm2;
float d2 = MIN( dist2(p3,pt), dist2(p4,pt) )/vec2_norm2;

float no_confidence_metric = MAX(sqrt(d1),sqrt(d2));

// If end point ratios are greater than .5 reject
if( no_confidence_metric < 0.5 && cosTheta < 0.707 )
return cv::Point (int(pt.x+0.5), int(pt.y+0.5));
}

return cv::Point(-1, -1);
}

sortPointsClockWise определяется как:

void sortPointsClockwise(cv::Point a[])
{
cv::Point b[4];

cv::Point ctr = (a[0]+a[1]+a[2]+a[3]);
ctr.x /= 4;
ctr.y /= 4;
b[0] = a[0]-ctr;
b[1] = a[1]-ctr;
b[2] = a[2]-ctr;
b[3] = a[3]-ctr;

for( int i=0; i<4; i++ )
{
if( b[i].x < 0 )
{
if( b[i].y < 0 )
a[0] = b[i]+ctr;
else
a[3] = b[i]+ctr;
}
else
{
if( b[i].y < 0 )
a[1] = b[i]+ctr;
else
a[2] = b[i]+ctr;
}
}

}

getArea определяется как:

float getArea(cv::Point arr[])
{
cv::Point  diag1 = arr[0]-arr[2];
cv::Point  diag2 = arr[1]-arr[3];

return 0.5*(diag1.cross(diag2));
}

isCloseBy определяется как:

bool isCloseBy( cv::Point p1, cv::Point p2 )
{
int D = 10;
// Checking that X values are within 10, same for Y values.
return ( abs(p1.x-p2.x)<=D && abs(p1.y-p2.y)<=D );
}

И наконец dist2:

float dist2( cv::Point p1, cv::Point p2 )
{
return float((p1.x-p2.x)*(p1.x-p2.x) + (p1.y-p2.y)*(p1.y-p2.y));
}

Вот несколько тестовых изображений и их результаты:




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

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

У кого-нибудь есть предложения?

Благодарю.

ПОПЫТКА РАСТВОРЕНИЯ ДХАНУШКИ

Mat gray, bw;     pyrDown(inImg, inImg);
cvtColor(inImg, gray, CV_RGB2GRAY);
int morph_size = 3;
Mat element = getStructuringElement( MORPH_ELLIPSE, cv::Size( 4*morph_size + 1, 2*morph_size+1 ), cv::Point( morph_size, morph_size ) );

morphologyEx(gray, gray, 2, element);
threshold(gray, bw, 160, 255, CV_THRESH_BINARY);

vector<vector<cv::Point> > contours;
vector<Vec4i> hierarchy;
findContours( bw, contours, hierarchy, CV_RETR_TREE, CV_CHAIN_APPROX_SIMPLE, cv::Point(0, 0) );

int largest_area=0;
int largest_contour_index=0;
cv::Rect bounding_rect;

for( int i = 0; i< contours.size(); i++ )
{
double a=contourArea( contours[i],false);  //  Find the area of contour
if(a>largest_area){
largest_area=a;
largest_contour_index=i;                //Store the index of largest contour
bounding_rect=boundingRect(contours[i]);
}
}

//Scalar color( 255,255,255);
rectangle(inImg, bounding_rect,  Scalar(0,255,0),1, 8,0);
Mat biggestRect = inImg(bounding_rect);
Mat card1 = biggestRect.clone();

5

Решение

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

    pyrDown(large, rgb0);
pyrDown(rgb0, rgb0);
pyrDown(rgb0, rgb0);

Mat small;
cvtColor(rgb0, small, CV_BGR2GRAY);

Mat morph;
Mat kernel = getStructuringElement(MORPH_ELLIPSE, Size(11, 11));
morphologyEx(small, morph, MORPH_OPEN, kernel);

Mat bw;
threshold(morph, bw, 0, 255.0, CV_THRESH_BINARY | CV_THRESH_OTSU);

Mat bdry;
kernel = getStructuringElement(MORPH_ELLIPSE, Size(3, 3));
erode(bw, bdry, kernel);
subtract(bw, bdry, bdry);

// do contour processing on bdry

Этот подход не будет работать в целом, поэтому я настоятельно рекомендую что-то вроде того, что предложил Рутгер.

введите описание изображения здесь

введите описание изображения здесь

введите описание изображения здесь

2

Другие решения

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

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

Чтобы добраться до этого, общий подход всегда один и тот же:

  • Для начала создайте большую базу тестовых изображений (не менее 100). Это определяет «нормальные» изображения, которые должны работать. Собирая изображения, вы уже начинаете думать о проблеме.
  • аннотируйте изображения, чтобы построить своего рода «основную правду». В этом случае «наземная правда» должна содержать 4 угла карты, поскольку это интересные моменты.
  • создайте приложение, которое запускает над этими изображениями алгоритм и сравнивает результат с основополагающей правдой. В этом случае «сравнение с наземной правдой» будет означать среднее расстояние найденных 4 угловых точек с наземными правдоподобными точками.
  • Выведите файл с разделителями табуляции, который вы называете .xls, и поэтому его можно открыть (в Windows) в Excel, дважды щелкнув. Хорошо, чтобы получить краткий обзор дел. Сначала посмотрим на худшие случаи. Затем откройте эти случаи вручную, чтобы попытаться понять, почему они не работают.
  • Теперь вы готовы изменить алгоритм. Измените что-нибудь и перезапустите. Сравните новый лист Excel со старым листом Excel. Теперь вы начинаете понимать компромиссы, которые вы должны сделать.

Тем не менее, я думаю, что вам нужно ответить на эти вопросы во время настройки алгоритма:

  • Вы позволяете немного сложить карты? Так нет абсолютно прямых линий? Если так, сконцентрируйтесь больше на углах вместо линий / краев.
  • Вы допускаете постепенные различия в освещении? Если это так, может помочь локальный фильтр растяжения контраста.
  • Вы допускаете тот же цвет для карты, что и фон? Если это так, вы должны сосредоточиться на содержимом карты, а не на границе карты.
  • Вы допускаете неидеальные линзы? Если да, то в какой степени?
  • Вы разрешаете вращать карты? Если да, то в какой степени?
  • Должен ли фон быть однородным по цвету и / или текстуре?
  • Насколько маленькой должна быть самая маленькая обнаруживаемая карта по отношению к размеру изображения? Если вы предполагаете, что по крайней мере 80% ширины или высоты должны быть покрыты, вы получаете устойчивость обратно.
  • Если на изображении видно более одной карты, должен ли алгоритм быть надежным и выбрать только одну, или все выходные данные в порядке?
  • Если карта не видна, должен ли он обнаружить этот случай? Встроенное обнаружение этого случая сделает его более удобным для пользователя («карта не найдена»), но также менее надежным.

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

Так что бы я выбрал? Исходя из трех предоставленных вами изображений, я бы начал с чего-то вроде:

  • Предположим, карты заполняют изображение от 50% до 100%.
  • Предположим, что карты повернуты максимум на 10 градусов или около того.
  • Предположим, что углы хорошо видны.
  • Предположим, что соотношение сторон (высота, деленная на ширину) карт составляет от 1/3 до 3.
  • Предположим, нет картоподобных объектов на заднем плане

Тогда алгоритм будет выглядеть так:

  • Определите в каждом квадранте изображения определенный угол с помощью углового фильтра. Так что в верхнем левом квадранте изображения находится левый верхний угол карты. Посмотрите, например, на http://www.ee.surrey.ac.uk/CVSSP/demos/corners/results3.html , или использовать функцию OpenCV для этого, как cornerHarris ,
  • Чтобы быть более надежным, рассчитайте более одного угла на квадрант.
  • Попробуйте построить параллелограммы с одним углом на каждый квадрант, комбинируя точки из каждого квадранта. Создать фитнес-функцию, которая дает более высокий балл:

    • с внутренними углами, близкими к 90 градусам
    • быть большим
    • При желании можно сравнить углы карты в зависимости от освещения или другой функции.

    Эта функция пригодности дает много возможностей настройки позже.

  • Верните параллелограмм с наибольшим количеством очков.

Так зачем использовать определение угла вместо грубого преобразования для обнаружения линии? По моему мнению, грубое преобразование (почти медленное) довольно чувствительно к шаблонам на заднем плане (что вы видите на своем первом изображении — оно обнаруживает более сильную линию на фоне, чем у карты), и не может хорошо обрабатывайте маленькие изогнутые линии, если только вы не используете бункер большего размера, что ухудшит обнаружение.

Удачи!

8

По вопросам рекламы ammmcru@yandex.ru
Adblock
detector