Finding closest non-black pixel in an image fast

前端 未结 9 1798
青春惊慌失措
青春惊慌失措 2020-12-16 21:22

I have a 2D image randomly and sparsely scattered with pixels.
given a point on the image, I need to find the distance to the closest pixel that is not in the background

相关标签:
9条回答
  • 2020-12-16 21:47

    Yes, the Nearest neighbor search is good, but does not guarantee you'll find the 'nearest'. Moving one pixel out each time will produce a square search - the diagonals will be farther away than the horizontal / vertical. If this is important, you'll want to verify - continue expanding until the absolute horizontal has a distance greater than the 'found' pixel, and then calculate distances on all non-black pixels that were located.

    0 讨论(0)
  • 2020-12-16 21:57

    As Pyro says, search the perimeter of a square that you keep moving out one pixel at a time from your original point (i.e. increasing the width and height by two pixels at a time). When you hit a non-black pixel, you calculate the distance (this is your first expensive calculation) and then continue searching outwards until the width of your box is twice the distance to the first found point (any points beyond this cannot possibly be closer than your original found pixel). Save any non-black points you find during this part, and then calculate each of their distances to see if any of them are closer than your original point.

    In an ideal find, you only have to make one expensive distance calculation.

    Update: Because you're calculating pixel-to-pixel distances here (instead of arbitrary precision floating point locations), you can speed up this algorithm substantially by using a pre-calculated lookup table (just a height-by-width array) to give you distance as a function of x and y. A 100x100 array costs you essentially 40K of memory and covers a 200x200 square around the original point, and spares you the cost of doing an expensive distance calculation (whether Pythagorean or matrix algebra) for every colored pixel you find. This array could even be pre-calculated and embedded in your app as a resource, to spare you the initial calculation time (this is probably serious overkill).

    Update 2: Also, there are ways to optimize searching the square perimeter. Your search should start at the four points that intersect the axes and move one pixel at a time towards the corners (you have 8 moving search points, which could easily make this more trouble than it's worth, depending on your application's requirements). As soon as you locate a colored pixel, there is no need to continue towards the corners, as the remaining points are all further from the origin.

    After the first found pixel, you can further restrict the additional search area required to the minimum by using the lookup table to ensure that each searched point is closer than the found point (again starting at the axes, and stopping when the distance limit is reached). This second optimization would probably be much too expensive to employ if you had to calculate each distance on the fly.

    If the nearest pixel is within the 200x200 box (or whatever size works for your data), you will only search within a circle bounded by the pixel, doing only lookups and <>comparisons.

    0 讨论(0)
  • 2020-12-16 21:59

    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).

    0 讨论(0)
  • 2020-12-16 22:05

    Personally, I'd ignore MusiGenesis' suggestion of a lookup table.

    Calculating the distance between pixels is not expensive, particularly as for this initial test you don't need the actual distance so there's no need to take the square root. You can work with distance^2, i.e:

    r^2 = dx^2 + dy^2
    

    Also, if you're going outwards one pixel at a time remember that:

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

    or if nx is the current value and ox is the previous value:

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

    It's easy to see how this works:

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

    So, in each iteration you therefore need only retain some intermediate variables thus:

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

    Tada! r^2 calculation with only bit shifts, adds and subtracts :)

    Of course, on any decent modern CPU calculating r^2 = dx*dx + dy*dy might be just as fast as this...

    0 讨论(0)
  • 2020-12-16 22:05

    Ok, this sounds interesting. I made a c++ version of a soulution, I don't know if this helps you. I think it works fast enough as it's almost instant on a 800*600 matrix. If you have any questions just ask.

    Sorry for any mistakes I've made, it's a 10min code... This is a iterative version (I was planing on making a recursive one too, but I've changed my mind). The algorithm could be improved by not adding any point to the points array that is to a larger distance from the starting point then the min_dist, but this involves calculating for each pixel (despite it's color) the distance from the starting point.

    Hope that helps

    //(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 讨论(0)
  • 2020-12-16 22:05

    I'm sure this could be done better but here's some code that searches the perimeter of a square around the centre pixel, examining the centre first and moving toward the corners. If a pixel isn't found the perimeter (radius) is expanded until either the radius limit is reached or a pixel is found. The first implementation was a loop doing a simple spiral around the centre point but as noted that doesn't find the absolute closest pixel. SomeBigObjCStruct's creation inside the loop was very slow - removing it from the loop made it good enough and the spiral approach is what got used. But here's this implementation anyway - beware, little to no testing done.

    It is all done with integer addition and subtraction.

    - (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 讨论(0)
提交回复
热议问题