快速查找图像中最接近的非黑色像素

时间:2008-11-21 00:50:32

标签: performance graphics 2d pixel

我有一个随机的2D图像,稀疏地散布着像素 给定图像上的一个点,我需要找到距离背景颜色最近的像素的距离(黑色) 最快的方法是什么?

我能想出的唯一方法是为像素构建一个kd树。但我真的想避免这种昂贵的预处理。而且,似乎一棵kd树给了我超过我需要的东西。我只需要与某种东西的距离,我不关心这是什么东西。

9 个答案:

答案 0 :(得分:7)

就个人而言,我忽略了MusiGenesis对查询表的建议。

计算像素之间的距离昂贵,特别是对于此初始测试,您不需要实际距离,因此无需取平方根。你可以使用距离^ 2,即:

r^2 = dx^2 + dy^2

另外,如果你一次向外走一个像素,请记住:

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

nx 是当前值且 ox 是先前的值:

    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
   }
}

多田! r ^ 2计算只有位移,加法和减法:)

当然,在任何体面的现代CPU上计算r ^ 2 = dx * dx + dy * dy可能与此一样快......

答案 1 :(得分:6)

正如Pyro所说,搜索一个正方形的周边,你一直从原点移出一个像素(即一次增加两个像素的宽度和高度)。当您点击非黑色像素时,您计算距离(这是您的第一个昂贵的计算),然后继续向外搜索,直到您的框的宽度是第一个找到点的距离的两倍(超出此范围的任何点都不可能更接近比你原来找到的像素)。保存在此部件中找到的任何非黑点,然后计算每个距离以查看它们中的任何一个是否比原始点更近。

在理想的发现中,您只需进行一次昂贵的距离计算。

更新:因为您在这里计算像素到像素的距离(而不是任意精度浮点位置),所以您可以通过使用预先计算的查找表来大幅加速此算法(只是一个逐个宽度的数组),给你距离作为 x y 的函数。 100x100阵列基本上花费40K内存,并且在原始点周围覆盖200x200平方,并且可以节省为找到的每个彩色像素进行昂贵的距离计算(无论是毕达哥拉斯算法还是矩阵代数)的成本。这个数组甚至可以预先计算并作为资源嵌入到您的应用程序中,以免您节省初始计算时间(这可能是严重的过度杀伤)。

更新2 :此外,还有一些方法可以优化搜索方形周长。你的搜索应该从与轴相交的四个点开始,一次向角落移动一个像素(你有8个移动的搜索点,这可能很容易使它比它的价值更麻烦,这取决于你的应用程序的要求)。一旦找到彩色像素,就不需要继续朝向角落,因为其余的点都离原点更远。

在第一个找到的像素之后,您可以使用查找表进一步将所需的附加搜索区域限制为最小值,以确保每个搜索点比找到的点更近(再次从轴开始,并在距离时停止)达到极限)。如果您必须动态计算每个距离,那么第二次优化可能会非常昂贵。

如果最近的像素位于200x200框内(或适用于您的数据的任何大小),则只会在由像素限定的圆圈内进行搜索,仅进行查找和&lt;&gt;比较。

答案 2 :(得分:2)

您没有指定测量距离的方式。我会假设L1(直线),因为它更容易;可能这些想法可以针对L2(欧几里德)进行修改。

如果您只对相对较少的像素执行此操作,则只需从源像素向外搜索螺旋,直到您遇到非黑色像素。

如果您正在为其中的许多/所有人执行此操作,请执行以下操作:构建图像大小的二维数组,其中每个单元格存储到最近的非黑色像素的距离(如果需要,还有坐标)那个像素)。做四行扫描:从左到右,从右到左,从下到上,从上到下。考虑从左到右的扫描;在扫描时,保持包含每行中看到的最后一个非黑色像素的一维列,并用该像素的距离和/或坐标标记二维数组中的每个单元格。为O(n ^ 2)。

或者,k-d树太过分了;你可以使用四叉树。编码比我的线扫描更难一点,更多的内存(但不到两倍),并且可能更快。

答案 3 :(得分:1)

搜索“最近邻搜索”,Google中的前两个链接可以为您提供帮助。

如果你只对每张图片1个像素进行此操作,我认为你最好的选择只是一个线性搜索,1个像素宽度的盒子向外。如果您的搜索框是方形的,则无法获取您找到的第一个点。你必须要小心

答案 4 :(得分:1)

是的,最近邻搜索是好的,但不保证你会找到'最近'。每次移出一个像素将产生方形搜索 - 对角线将比水平/垂直更远。如果这很重要,您需要验证 - 继续扩展,直到绝对水平的距离大于“找到的”像素,然后计算所有非黑色像素的距离。

答案 5 :(得分:1)

好的,这听起来很有趣。 我制作了一个c ++版本的soulution,我不知道这是否对你有所帮助。我认为它的工作速度足够快,因为它在800 * 600矩阵上几乎是即时的。如果您有任何问题,请询问。

对不起,我犯了任何错误,这是一个10分钟的代码...... 这是一个迭代版本(我也在制作一个递归版本,但我已经改变了主意)。 可以通过不向点阵列添加任何点来改进算法,该点距离起点和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;
}

答案 6 :(得分: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;

}

答案 7 :(得分:0)

You can combine many ways to speed it up.

  • A way to accelerate the pixel lookup is to use what I call a spatial lookup map. It is basically a downsampled map (say of 8x8 pixels, but its a tradeoff) of the pixels in that block. Values can be "no pixels set" "partial pixels set" "all pixels set". This way one read can tell if a block/cell is either full, partially full or empty.
  • scanning a box/rectangle around the center may not be ideal because there are many pixels/cells which are far far away. I use a circle drawing algorithm (Bresenham) to reduce the overhead.
  • reading the raw pixel values can happen in horizontal batches, for example a byte (for a cell size of 8x8 or multiples of it), dword or long. This should give you a serious speedup again.
  • you can also use multiple levels of "spatial lookup maps", its again a tradeoff.

For the distance calculatation the mentioned lookup table can be used, but its a (cache)bandwidth vs calculation speed tradeoff (I dunno how it performs on a GPU for example).

答案 8 :(得分:-1)

我会做一个简单的查找表 - 对于每个像素,预先计算到最近的非黑色像素的距离,并将值存储在与相应像素相同的偏移量中。当然,这样你需要更多的内存。