среда, 30 июля 2014 г.
среда, 9 июля 2014 г.
понедельник, 7 июля 2014 г.
Маркер дополненной реальности.(часть 1)
Маркер дополненной реальности.(часть 1)
Всем привет. Хочу выложить сюда подробную инструкцию: как создать маркер дополненной реальности средствами OpenCV. Сперва давайте в общих чертах определимся с алгоритмом:
1) обесцветить картинку
2) сделать её чёрно-белой(реально два цвета, без серого и т.д)
3) найти контуры областей одного света(чёрных или белых)
4) взять только квадратные области
5) разогнуть картинку с маркера
6) понять тот ли это маркер
Теперь пойдём по этим пунктам с подробным разбором:
1 шаг:
Сначала мы решили обесцветить изображение, чтобы не тащить за собой ненужную информацию о цвете, а так же упростить себе дальнейшую обработку. Теперь давайте посмотрим как обесцветить изображение средствами OpenCV:
cvCvtColor(frame, img_gray, CV_RGB2GRAY);
Первым параметром мы указываем исходное изображение, вторым одноканальное чёрно-белое изображение. Создать(выделить память) черно-белое изображение можно так:
IplImage * img_gray = cvCreateImage( cvSize(200, 200) , 8, 1);
Здесь cvSize создает объект типа MCvSize, который указывает размеры нашего изображения 200 на 200.(http://www.emgu.com/wiki/files/1.3.0.0/html/db100c8e-600f-2e19-0604-bffba80ef50d.htm)
2 шаг:
Теперь мы должны возвести все цвета в абсолют. Теперь пикселы либо чёрные либо белые. На этом этапе уже становиться интуитивно понятно как действовать дальше, ведь теперь мы можем ясно различить квадратик на листе и теперь останется его только вычленить из кадра.
Вернёмся к технической стороне вопроса: есть несколько алгоритмов позволяющих получить из обычного изображения вотэто, но всегда мы будем пользоваться следующим кодом:
cvEqualizeHist(img_gray, img_hist);
cvThreshold(img_hist, img_thr, thresholdBlak, 255, CV_THRESH_BINARY);
Первая функция нормализует яркость и увеличивает контрастность изображения с которым мы собираемся работать дальше(http://www.emgu.com/wiki/files/1.3.0.0/html/20b60ec4-42f7-10c1-88a9-4f776a11ce5d.htm). Вторую стоит рассмотреть чуть подробнее. Первые два параметра - это исходное изображения и изображение, куда будет записан результат. Кстати, если у вас будет всё "взрываться" при запуске приложения, убедитесь что при вызове cvCreateImage вы указали одинаковое количество каналов(3 параметр) для источника и результата. В качестве следующего параметра cvThreshold принимает пороговое значение для разграничения того, какие пикселы будут черные, а какие белые. Всё, что ниже этого порога чёрное, выше белое. На начальном этапе можно оставить там 127, что является серединой цветового диапозона 0...255 одноканальных изображений. Но в примере кода там указана переменная thresholdBlak. Это усовершенствование введено для динамической подстройки под освещение в помещение. Например если слишком наклонить листок с маркером, то тень может внести помехи и мы не увидим нашего маркера, приходиться менять порог, чтобы тень не считалась чёрным пятном. С другой стороны мы можем поднести маркер к свету, тогда блики и отражённый свет так же будут мешать и понадобиться повысить порог. В своём текущем проекте я использовал следующий алгоритм: если на кадре не найдено ни одного маркера, то значение thresholdBlak начинает бегать в диапазоне от 50 до 250, и таким образом находиться порог при котором маркер видим. При таком способе подстройки порога важно учесть, что, например при движение, маркер будет распознаваться с перерывами(в некоторых кадрах его не получиться распознать), и чтобы пороговое значение не сбилось в этом случае, мы вводим таймер. Если по истечение этого таймера не удалось найти маркер, то только в этом случае запускаем коррекцию порога.
Вернёмся снова к cvThreshold, 4 параметр указывает максимальное значение цвета(у нас это 255 для одного канала). Интерес вызывает 5 параметр, он указывает способ которым определяется какие пикселы чёрные, а какие белые. Я не буду описывать здесь все способы, статья и так вышла громоздкая, приведу лишь их перечень, а опробовать предоставлю вам в качестве упражнения:
#define CV_THRESH_BINARY 0 /* value = value > threshold ? max_value : 0 */
#define CV_THRESH_BINARY_INV 1 /* value = value > threshold ? 0 : max_value */
#define CV_THRESH_TRUNC 2 /* value = value > threshold ? threshold : value */
#define CV_THRESH_TOZERO 3 /* value = value > threshold ? value : 0 */
#define CV_THRESH_TOZERO_INV 4 /* value = value > threshold ? 0 : value */
#define CV_THRESH_MASK 7
#define CV_THRESH_OTSU 8 /* use Otsu algorithm to choose the optimal threshold value;
combine the flag with one of the above CV_THRESH_* values */3 шаг:
На этом этапе мы хотим получить контуры изображения. Здесь нужно знать про хранилища памяти и последовательности, я предлагаю ознакомиться с ними здесь: http://robocraft.ru/blog/computervision/603.html
Теперь создадим хранилище памяти для найденных контуров:
CvMemStorage * storage;
storage = cvCreateMemStorage(0);
Теперь создадим последовательность контуров, которые будут лежать в этом хранилище и заполним её:
CvSeq * contours = NULL;
cvFindContours(img_thr, storage, &contours);
Вышеприведённая функция ищет в указанном двухцветном изображение контуры и запоминает их в последовательность. Теперь когда у нас есть последовательность контуров, нужно отфильтровать её и найти только тот контур, который нам нужен.
4 шаг:
Для прохода по всем элементам последовательности, можно использовать такой цикл:
while (contours) {
contours = contours->h_next;
}
Т.к. это список мы просто каждый раз переходим к следующему элементу списка, пока не дойдём до пустого(NULL). Теперь мы хотим обработать только выпуклые четырёхугольники:
CvSeq * result = cvApproxPoly(contours, sizeof(CvContour), storage, CV_POLY_APPROX_DP, cvContourPerimeter(contours)*0.02, 0);
if (result->total==4 && cvContourArea(result) >= 100 && cvCheckContourConvexity(result)) {
if (result->total==4 && cvContourArea(result) >= 100 && cvCheckContourConvexity(result)) {
....
}
cvApproxPoly - служит для сглаживания и получения более аккуратных контуров, принимает в себя последовательность контуров, размер заголовка аппроксимирующей кривой, хранилище, способ аппроксимации, текущий параметр соответствует алгоритму Дугласа – Пейкера. Следующий параметр задаёт точность аппроксимации, мы указываем равную периметру контура с погрешностью 0.02. Теперь в условии берём только те контуры, у которых четыре угла, площадь которых больше 100(чтобы отбросить точки и прочий мусор) и только те контуры, которые выпуклые.
5 шаг:
Теперь, когда мы отсеяли контуры и получили только те, которые скорее всего являются маркерами. Нужно уточнить, а маркер ли это. Для этого нужно проанализировать то, что находиться внутри контура. Но на экране маркер может быть повернут или наклонен, значит нужно найти способ развернуть его к исходному виду. Такой способ называется афинные преобразования. Это такие преобразования геометрии, при которых образом прямой является прямая. То есть инвариантом является сорт фигуры.
Нам нужно найти матрицу для этих преобразований, которая будет переводить нашу фигуру в квадрат 200х200:
Сначала нужно создать массивы координат исходной геометрии и той, которую мы хотим получить преобразованиями:
CvPoint2D32f srcQuad[4];
CvPoint2D32f dstQuad[4];
dstQuad[0].x = 0;
dstQuad[0].y = 0; //верхний левый угол
dstQuad[1].x = 200;
dstQuad[1].y = 0; //верхний правый угол
dstQuad[2].x = 0;
dstQuad[2].y = 200; //нижний левый угол
dstQuad[3].x = 200;
dstQuad[3].y = 200; //нижний правый угол
Мы создали массивы для координат вершин и заполнили массив желаемых координат.
Теперь нужно заполнить массив исходных координат:
CvPoint* p[4];
p[0] = (CvPoint*)cvGetSeqElem(result, 0);
p[1] = (CvPoint*)cvGetSeqElem(result, 1);
p[2] = (CvPoint*)cvGetSeqElem(result, 2);
p[3] = (CvPoint*)cvGetSeqElem(result, 3);
srcQuad[1].x = (float)p[0]->x;
srcQuad[1].y = (float)p[0]->y;
srcQuad[0].x = (float)p[1]->x;
srcQuad[0].y = (float)p[1]->y;
srcQuad[2].x = (float)p[2]->x;
srcQuad[2].y = (float)p[2]->y;
srcQuad[3].x = (float)p[3]->x;
srcQuad[3].y = (float)p[3]->y;
Мы получили точки контура используя вызов cvGetSeqElem. После записали их координаты в массив. 0 и 1 в индексах массива поменяны намеренно, так как важен порядок следования координат.
IplImage * mrk = cvCreateImage(cvSize(200, 200), 8, 1);
CvMat * warp_mat = cvCreateMat(3, 3, CV_32FC1);
cvGetPerspectiveTransform(srcQuad, dstQuad, warp_mat);
cvWarpPerspective(img_thr_bkp, mrk, warp_mat);
Тут мы создали картинку куда будем сохранять полученное изображение и матрицу, которая будет использована для афинных преобразований. Далее на основе массивов исходных и желаемых координат вершин заполняется матрица вызовом cvGetPerspectiveTransform и применяется к исходному изображению вырезая и выпрямляя область, содержащую маркер.
6 шаг:
Теперь нужно понять какой из найденных маркеров наш. Я решал этот вопрос следующим образом. Класс, который занимается поиском маркеров принимает в себя функцию-фильтр(точнее указатель на неё):
typedef bool (*FilterFunctionPTR)(IplImage *);
И после того как маркер найден, к нему применяется функция-фильтр, которая определяет будем ли мы считать его верным. Если маркер удовлетворяет нашим критериям мы должны вернуть true. Приведу пример фильтра которым я пользовался для поиска маркера:
bool isCapture(IplImage * frame) {
int countE = 0;
for(int y = 0; y < 200; y++) {
uchar* ptr_Frame = (uchar*) (frame->imageData + y * frame->widthStep);
if(0 == ptr_Frame[10]) {
countE++;
}
if(0 == ptr_Frame[190]) {
countE++;
}
}
uchar* ptr_Frame = (uchar*) (frame->imageData + 10 * frame->widthStep);
for(int x = 0; x < 200; x++) {
if(0 == ptr_Frame[x]) {
countE++;
}
}
ptr_Frame = (uchar*) (frame->imageData + 190 * frame->widthStep);
for(int x = 0; x < 200; x++) {
if(0 == ptr_Frame[x]) {
countE++;
}
}
return (countE > 650);
}
bool haseColor(IplImage * frame, int x1, int y1, int x2, int y2, int S, int c) {
int count = 0;
for(int y = y1; y < y2; y++) {
uchar* ptr_Frame = (uchar*) (frame->imageData + y * frame->widthStep);
for(int x = x1; x < x2; x++) {
if(c == ptr_Frame[x2]) {
count++;
}
}
}
return (S - count > 20*20);
}
bool haseBlack(IplImage * frame, int x1, int y1, int x2, int y2, int S) {
return haseColor(frame, x1, y1, x2, y2, S, 0);
}
bool haseWhite(IplImage * frame, int x1, int y1, int x2, int y2, int S) {
return haseColor(frame, x1, y1, x2, y2, S, 255);
}
bool captureImageTest(IplImage * frame) {
return haseBlack(frame, 100, 50, 130, 90, 1200) && haseBlack(frame, 100, 120, 130, 160, 1200) && haseWhite(frame, 30, 30, 70, 165, 3800);
}
bool markerFilter(IplImage * frame) {
bool result = false;
if(!isCapture(frame)) {
return false;
}
result = result || captureImageTest(frame);
cvFlip(frame, frame, 1);
result = result || captureImageTest(frame);
cvFlip(frame, frame, 1);
result = result || captureImageTest(frame);
cvFlip(frame, frame, 1);
result = result || captureImageTest(frame);
return result;
}
Подписаться на:
Комментарии (Atom)




























