2008-11-21 19 views
5

У меня есть 2D-изображение, случайно и редко рассеянное с пикселями.
Учитывая точку на изображении, мне нужно найти расстояние до ближайшего пикселя, который не находится в фоновом цвете (черный).
Каков самый быстрый способ сделать это?Поиск ближайшего нечерного пикселя в изображении быстро

Единственный способ, с помощью которого я мог придумать, - построить kd-дерево для пикселей. но я бы очень хотел избежать такой дорогостоящей предварительной обработки. Кроме того, кажется, что kd-дерево дает мне больше, чем мне нужно. Мне нужно только расстояние, и мне все равно, что это такое.

ответ

6

Как пишет Pyro, выполните поиск по периметру квадрата, который вы перемещаете по одному пикселю за раз от исходной точки (т. Е. Увеличивая ширину и высоту на два пикселя за раз). Когда вы нажимаете нечерный пиксель, вы вычисляете расстояние (это ваш первый дорогой расчет), а затем продолжайте поиск вовне, пока ширина вашего окна не будет вдвое больше расстояния до первой найденной точки (любые точки за пределами этого не могут быть ближе чем ваш оригинальный найденный пиксель). Сохраните любые нечерные точки, которые вы найдете во время этой части, а затем вычислите каждое их расстояние, чтобы узнать, ближе ли они к вашей исходной точке.

В идеальной поисковой системе вам нужно сделать только один дорогой расчет расстояния.

Update: Потому что вы расчет пиксель к пикселю расстояния здесь (вместо произвольной точности с плавающей запятой) местоположения, вы можете ускорить этот алгоритм существенно, используя заранее рассчитанную таблицу поиска (только по высоте по ширине), чтобы дать вам расстояние от x и y. Размер массива 100x100 стоит, по существу, 40 Кбайт памяти и покрывает площадь 200х200 вокруг исходной точки, и избавляет вас от стоимости дорогостоящего вычисления расстояния (будь то пифагорейская или матричная алгебра) для каждого цветного пикселя, который вы находите. Этот массив может даже быть предварительно рассчитан и встроен в ваше приложение в качестве ресурса, чтобы избавить вас от первоначального времени вычисления (это, вероятно, серьезный перебор).

Обновление 2: Также существуют способы оптимизации поиска по периметру площади. Ваш поиск должен начинаться с четырех точек, которые пересекают оси и перемещают один пиксель за раз по углам (у вас есть 8 движущихся точек поиска, которые могут легко сделать это больше проблем, чем это стоит, в зависимости от требований вашего приложения). Как только вы обнаружите цветной пиксель, нет необходимости продолжать движение по углам, так как остальные точки находятся дальше от начала координат.

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

Если ближайший пиксель находится в поле 200x200 (или какой размер работает для ваших данных), вы будете искать только в пределах круга, ограниченного пикселем, делая только поисковые запросы и <> сравнения.

+0

Это на самом деле лучший ответ. Спасибо. – shoosh

1

Поиск "Поиск ближайшего соседа", первые две ссылки в Google должны вам помочь.

Если вы делаете это только для 1 пикселя на изображение, я думаю, что ваш лучший выбор - это просто линейный поиск, 1 пиксельная ширина окна вовне. Вы не можете взять первую точку, которую вы найдете, если поле поиска квадратное. Вы должны быть осторожны

1

Да, поиск ближайших соседей хорош, но не гарантирует, что вы найдете «ближайший». Каждый раз перемещаясь по одному пикселю, вы получите квадратный поиск - диагонали будут дальше, чем горизонтальные/вертикальные. Если это важно, вам нужно проверить - продолжить расширение до тех пор, пока абсолютная горизонталь не будет больше, чем «найденный» пиксель, а затем вычислить расстояния на всех нечерных пикселях, которые были расположены.

2

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

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

Если вы делаете это для многих/всех из них, как насчет этого: Создайте 2-мерный массив размером изображения, где каждая ячейка сохраняет расстояние до ближайшего нечерного пикселя (и, если необходимо, координаты этого пикселя). Сделайте четыре линии развертки: слева направо, справа налево, снизу вверх и сверху вниз. Рассмотрите движение слева направо; по мере того, как вы подметаете, держите столбец 1-D, содержащий последний нечерный пиксель, видимый в каждой строке, и отметьте каждую ячейку в 2-мерном массиве расстоянием до и/или координатами этого пикселя. O (N^2).

В качестве альтернативы, дерево k-d является излишним; вы можете использовать квадрант. Только немного сложнее кодировать, чем моя линия, немного больше памяти (но меньше, чем в два раза больше) и, возможно, быстрее.

+0

Я не вижу правильности алгоритма развертки. Если ближайший пиксель находится в некотором диагональном направлении, тогда этот метод не найдет его. Он найдет только пиксели на двух осях, идущих от точки. – shoosh

-1

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

+0

Цель состоит в том, чтобы * избегать длительных операций обработки, которые являются О (число точек) – shoosh

+0

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

5

Лично я бы проигнорировал предложение MusiGenesis о справочной таблице.

Расчет расстояния между пикселями не дорогой, особенно, поскольку для этого первоначального теста вам не требуется фактическое расстояние, поэтому нет необходимости принимать квадратный корень. Вы можете работать с расстоянием^2, а именно:

r^2 = dx^2 + dy^2 

Кроме того, если вы собираетесь наружу один пиксель за один раз помните, что:

(n + 1)^2 = n^2 + 2n + 1 

или если пх текущее значение и вол является предыдущим значением:

nx^2 = ox^2 + 2ox + 1 
      = ox^2 + 2(nx - 1) + 1 
      = ox^2 + 2nx - 1 
=> nx^2 += 2nx - 1 

это легко увидеть, как это работает:

1^2 = 0 + 2*1 - 1 = 1 
2^2 = 1 + 2*2 - 1 = 4 
3^2 = 4 + 2*3 - 1 = 9 
4^2 = 9 + 2*4 - 1 = 16 
5^2 = 16 + 2*5 - 1 = 25 
etc... 

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

int dx2 = 0, dy2, r2; 
for (dx = 1; dx < w; ++dx) { // ignoring bounds checks 
    dx2 += (dx << 1) - 1; 
    dy2 = 0; 
    for (dy = 1; dy < h; ++dy) { 
     dy2 += (dy << 1) - 1; 
     r2 = dx2 + dy2; 
     // do tests here 
    } 
} 

Тада!г^2 вычисления только с битовыми сдвигами, добавляет и вычитает :)

Конечно, на любом приличном современном процессоре вычисления г^2 = дх * дх + ду * ду может быть столь же быстро, как это ...

1

Хорошо, это звучит интересно. Я сделал C++ версию души, я не знаю, поможет ли это вам. Я думаю, что он работает достаточно быстро, так как почти мгновенно работает на матрице 800 * 600. Если у вас есть какие-либо вопросы просто спросить.

Извините за ошибки, которые я сделал, это код 10min ... Это итеративная версия (я тоже планировал сделать рекурсивный, но я передумал). Алгоритм можно улучшить, не добавляя никакой точки в массив точек, который находится на большем расстоянии от начальной точки, а затем min_dist, но это предполагает вычисление для каждого пикселя (несмотря на его цвет) расстояния от начальной точки.

Надежда, что помогает

//(c++ version) 
#include<iostream> 
#include<cmath> 
#include<ctime> 
using namespace std; 
//ITERATIVE VERSION 

//picture witdh&height 
#define width 800 
#define height 600 
//indexex 
int i,j; 

//initial point coordinates 
int x,y; 
//variables to work with the array 
int p,u; 
//minimum dist 
double min_dist=2000000000; 
//array for memorising the points added 
struct point{ 
    int x; 
    int y; 
} points[width*height]; 
double dist; 
bool viz[width][height]; 
// direction vectors, used for adding adjacent points in the "points" array. 
int dx[8]={1,1,0,-1,-1,-1,0,1}; 
int dy[8]={0,1,1,1,0,-1,-1,-1}; 
int k,nX,nY; 
//we will generate an image with white&black pixels (0&1) 
bool image[width-1][height-1]; 
int main(){ 
    srand(time(0)); 
    //generate the random pic 
    for(i=1;i<=width-1;i++) 
     for(j=1;j<=height-1;j++) 
      if(rand()%10001<=9999) //9999/10000 chances of generating a black pixel 
      image[i][j]=0; 
      else image[i][j]=1; 
    //random coordinates for starting x&y 
    x=rand()%width; 
    y=rand()%height; 
    p=1;u=1; 
    points[1].x=x; 
    points[1].y=y; 
    while(p<=u){ 
     for(k=0;k<=7;k++){ 
      nX=points[p].x+dx[k]; 
      nY=points[p].y+dy[k]; 
      //nX&nY are the coordinates for the next point 
      //if we haven't added the point yet 
      //also check if the point is valid 
      if(nX>0&&nY>0&&nX<width&&nY<height) 
      if(viz[nX][nY] == 0){ 
       //mark it as added 
       viz[nX][nY]=1; 
       //add it in the array 
       u++; 
       points[u].x=nX; 
       points[u].y=nY; 
       //if it's not black 
       if(image[nX][nY]!=0){ 
       //calculate the distance 
       dist=(x-nX)*(x-nX) + (y-nY)*(y-nY); 
       dist=sqrt(dist); 
       //if the dist is shorter than the minimum, we save it 
       if(dist<min_dist) 
        min_dist=dist; 
        //you could save the coordinates of the point that has 
        //the minimum distance too, like sX=nX;, sY=nY; 
       } 
      } 
     } 
     p++; 
} 
    cout<<"Minimum dist:"<<min_dist<<"\n"; 
return 0; 
} 
0

Я уверен, что это можно было бы сделать лучше, но вот некоторый код, который выполняет поиск по периметру квадрата вокруг центральной точки, рассматривая центр первого и двигаясь по направлению к углам. Если пиксель не найден, периметр (радиус) расширяется до тех пор, пока не будет достигнут либо предел радиуса, либо не будет найден пиксель. Первой реализацией был цикл, делающий простую спираль вокруг центральной точки, но, как отмечалось, не находит абсолютный ближайший пиксель. Создание SomeBigObjCStruct внутри цикла было очень медленным - удаление его из цикла сделало его достаточно хорошим, и спиральный подход стал использоваться. Но вот эта реализация в любом случае - будьте осторожны, мало что не проведет.

Все это делается с помощью целочисленного сложения и вычитания.

- (SomeBigObjCStruct *)nearestWalkablePoint:(SomeBigObjCStruct)point {  

typedef struct _testPoint { // using the IYMapPoint object here is very slow 
    int x; 
    int y; 
} testPoint; 

// see if the point supplied is walkable 
testPoint centre; 
centre.x = point.x; 
centre.y = point.y; 

NSMutableData *map = [self getWalkingMapDataForLevelId:point.levelId]; 

// check point for walkable (case radius = 0) 
if(testThePoint(centre.x, centre.y, map) != 0) // bullseye 
    return point; 

// radius is the distance from the location of point. A square is checked on each iteration, radius units from point. 
// The point with y=0 or x=0 distance is checked first, i.e. the centre of the side of the square. A cursor variable 
// is used to move along the side of the square looking for a walkable point. This proceeds until a walkable point 
// is found or the side is exhausted. Sides are checked until radius is exhausted at which point the search fails. 
int radius = 1; 

BOOL leftWithinMap = YES, rightWithinMap = YES, upWithinMap = YES, downWithinMap = YES; 

testPoint leftCentre, upCentre, rightCentre, downCentre; 
testPoint leftUp, leftDown, rightUp, rightDown; 
testPoint upLeft, upRight, downLeft, downRight; 

leftCentre = rightCentre = upCentre = downCentre = centre; 

int foundX = -1; 
int foundY = -1; 

while(radius < 1000) { 

    // radius increases. move centres outward 
    if(leftWithinMap == YES) { 

     leftCentre.x -= 1; // move left 

     if(leftCentre.x < 0) { 

      leftWithinMap = NO; 
     } 
    } 

    if(rightWithinMap == YES) { 

     rightCentre.x += 1; // move right 

     if(!(rightCentre.x < kIYMapWidth)) { 

      rightWithinMap = NO; 
     } 
    } 

    if(upWithinMap == YES) { 

     upCentre.y -= 1; // move up 

     if(upCentre.y < 0) { 

      upWithinMap = NO; 
     } 
    } 

    if(downWithinMap == YES) { 

     downCentre.y += 1; // move down 

     if(!(downCentre.y < kIYMapHeight)) { 

      downWithinMap = NO; 
     } 
    } 

    // set up cursor values for checking along the sides of the square 
    leftUp = leftDown = leftCentre; 
    leftUp.y -= 1; 
    leftDown.y += 1; 
    rightUp = rightDown = rightCentre; 
    rightUp.y -= 1; 
    rightDown.y += 1; 
    upRight = upLeft = upCentre; 
    upRight.x += 1; 
    upLeft.x -= 1; 
    downRight = downLeft = downCentre; 
    downRight.x += 1; 
    downLeft.x -= 1; 

    // check centres 
    if(testThePoint(leftCentre.x, leftCentre.y, map) != 0) { 

     foundX = leftCentre.x; 
     foundY = leftCentre.y; 
     break; 
    } 
    if(testThePoint(rightCentre.x, rightCentre.y, map) != 0) { 

     foundX = rightCentre.x; 
     foundY = rightCentre.y; 
     break; 
    } 
    if(testThePoint(upCentre.x, upCentre.y, map) != 0) { 

     foundX = upCentre.x; 
     foundY = upCentre.y; 
     break; 
    } 
    if(testThePoint(downCentre.x, downCentre.y, map) != 0) { 

     foundX = downCentre.x; 
     foundY = downCentre.y; 
     break; 
    } 

    int i; 

    for(i = 0; i < radius; i++) { 

     if(leftWithinMap == YES) { 
      // LEFT Side - stop short of top/bottom rows because up/down horizontal cursors check that line 
      // if cursor position is within map 
      if(i < radius - 1) { 

       if(leftUp.y > 0) { 
        // check it 
        if(testThePoint(leftUp.x, leftUp.y, map) != 0) { 
         foundX = leftUp.x; 
         foundY = leftUp.y; 
         break; 
        } 
        leftUp.y -= 1; // moving up 
       } 
       if(leftDown.y < kIYMapHeight) { 
        // check it 
        if(testThePoint(leftDown.x, leftDown.y, map) != 0) { 
         foundX = leftDown.x; 
         foundY = leftDown.y; 
         break; 
        } 
        leftDown.y += 1; // moving down 
       } 
      } 
     } 

     if(rightWithinMap == YES) { 
      // RIGHT Side 
      if(i < radius - 1) { 

       if(rightUp.y > 0) { 

        if(testThePoint(rightUp.x, rightUp.y, map) != 0) { 
         foundX = rightUp.x; 
         foundY = rightUp.y; 
         break; 
        } 
        rightUp.y -= 1; // moving up 
       } 
       if(rightDown.y < kIYMapHeight) { 

        if(testThePoint(rightDown.x, rightDown.y, map) != 0) { 
         foundX = rightDown.x; 
         foundY = rightDown.y; 
         break; 
        } 
        rightDown.y += 1; // moving down 
       } 
      } 
     } 

     if(upWithinMap == YES) { 
      // UP Side 
      if(upRight.x < kIYMapWidth) { 

       if(testThePoint(upRight.x, upRight.y, map) != 0) { 
        foundX = upRight.x; 
        foundY = upRight.y; 
        break; 
       } 
       upRight.x += 1; // moving right 
      } 
      if(upLeft.x > 0) { 

       if(testThePoint(upLeft.x, upLeft.y, map) != 0) { 
        foundX = upLeft.x; 
        foundY = upLeft.y; 
        break; 
       } 
       upLeft.y -= 1; // moving left 
      } 
     } 

     if(downWithinMap == YES) { 
      // DOWN Side 
      if(downRight.x < kIYMapWidth) { 

       if(testThePoint(downRight.x, downRight.y, map) != 0) { 
        foundX = downRight.x; 
        foundY = downRight.y; 
        break; 
       } 
       downRight.x += 1; // moving right 
      } 
      if(downLeft.x > 0) { 

       if(testThePoint(upLeft.x, upLeft.y, map) != 0) { 
        foundX = downLeft.x; 
        foundY = downLeft.y; 
        break; 
       } 
       downLeft.y -= 1; // moving left 
      } 
     } 
    } 

    if(foundX != -1 && foundY != -1) { 
     break; 
    } 

    radius++; 
} 

// build the return object 
if(foundX != -1 && foundY != -1) { 

    SomeBigObjCStruct *foundPoint = [SomeBigObjCStruct mapPointWithX:foundX Y:foundY levelId:point.levelId]; 
    foundPoint.z = [self zWithLevelId:point.levelId]; 
    return foundPoint; 
} 
return nil; 

}

0

Вы можете объединить много способов ускорить его.

  • Способ ускорения поиска пикселей - использовать то, что я называю пространственной поисковой картой. В основном это карта с пониженной выборкой (например, 8х8 пикселей, но ее компромисс) пикселей в этом блоке. Значения могут быть «без пикселей» «частичный набор пикселей» «все пиксели установлены». Таким образом, один прочитанный может определить, является ли блок/ячейка полной, частично полной или пустой.
  • Сканирование коробки/прямоугольника вокруг центра может быть не идеальным, потому что есть много пикселей/ячеек, которые находятся далеко. Я использую алгоритм рисования окружности (Bresenham), чтобы уменьшить накладные расходы.
  • чтение значений исходного пикселя может происходить в горизонтальных партиях, например, в байтах (для размера ячейки 8x8 или кратных его), dword или long. Это должно привести к серьезному ускорению.
  • вы также можете использовать несколько уровней «пространственных карт поиска», это снова компромисс.

Для расчета расчёта можно использовать указанную таблицу поиска, но ее пропускную способность (кэш) и соотношение скорости вычислений (например, как это работает на графическом процессоре).