代码之家  ›  专栏  ›  技术社区  ›  malaki1974

选择相似的像素,围绕像素绘制Bezier路径并填充颜色-iOS

  •  -3
  • malaki1974  · 技术社区  · 8 年前

    假设一个黑色背景上有彩色形状的JPG。有些形状摸起来。我想点击一个形状,然后选择所有颜色和轮廓相似的像素,在相同的位置填充一个新的图层。可以是uiview,也可以是uiimage。

    对类似颜色选择和轮廓填充的方向表示赞赏。

    2 回复  |  直到 8 年前
        1
  •  1
  •   James Bucanek    8 年前

    不是完全相同的问题,但相似:我的应用程序需要围绕具有透明背景的图标的边缘创建一条贝塞尔路径。实际上,这两个问题之间唯一的区别就是你选择的标准来决定哪些像素是“形状”的一部分,哪些不是。

    该算法使用一种称为Moore-Nieghbor跟踪的简单技术。从本质上来说,想象一个盲人在建筑物的边缘行走,一只手放在墙上,直到他们回到起始位置。生成的路径是建筑物的轮廓。

    这段代码有点复杂,因为我引入了一个“inset”值,它可以有效地缩小轮廓n个像素;我这样做是为了在更高的分辨率下,路径会覆盖图标的边缘,而不是在图标的外部。如果您不需要的话,应该很容易绕过这个代码。

    我的解决方案基于一个名为 SystemIconBuffer 它拥有图标的多个分辨率。你基本上可以忽略所有这些。重要的一步是将图像栅格化为一个数组(我使用 CGBitmatContextCreate )并获取指向该数组的指针 srcBuffer .

    最后,我的图像总是正方形的,所以我的代码只有一维值( dim )你需要转换成 dimX dimY ,对于矩形图像。

    再说一遍,我的代码只对找到图像的透明边缘感兴趣,所以它只关注每个像素的“alpha”分量。根据需要调整您的标准。

    不用再多费吹灰之力…

    // Based on Moore-Nieghbor tracing
    //  <http://www.imageprocessingplace.com/downloads_V3/root_downloads/tutorials/contour_tracing_Abeer_George_Ghuneim/moore.html>
    //  <https://en.wikipedia.org/wiki/Moore_neighborhood>
    
    // RGBA buffers store each pixel value as four sequential 8-bit integers
    // The outline trace is interested only in the alpha component, and uses the other three bytes for intermediate values
    #define kAlphaOffset        3
    #define kBlackOffset        0       // reuse R to record if this pixel is considered to be "black"
    #define kVisitedOffset      2       // reuse G to record if this pixel has been visited
    
    #define kIsBodyAlphaThreshold   (255/20)    // a pixel with > 5% opacity is considered to be "black" (part of the image)
    
    // Notes: all of these functions assume a square image buffer exactly dim x dim x RGBA
    //        all functions clip x & y to valid pixels addresses in the buffer and ignore anything outside
    static NSUInteger GetValue( NSInteger x, NSInteger y, NSUInteger offset, const UInt8* buffer, NSInteger dim );
    static void FillSquareOfValues( NSInteger x, NSInteger y, NSInteger size, NSUInteger value, NSUInteger offset, UInt8* buffer, NSInteger dim );
    
    #define kNeighbors      8   // each pixel has eight Moore neighbors
    // The x & y offset of neighboring pixels, starting with the pixel immediately above the pixel and moving clockwise around it
    enum {
        kUpNeighbor = 0,
        kUpperRightNeighbor,
        kRightNeighbor,
        kLowerRightNeighbor,
        kDownNeighbor,
        kLowerLeftNeighbor,
        kLeftNeighbor,
        kUpperLeftNeighbor
    };
    static NSInteger NeighborX[kNeighbors] = { 0,  1,  1,  1,  0, -1, -1, -1 };     // coordinate offsets of neighbor directions
    static NSInteger NeighborY[kNeighbors] = { 1,  1,  0, -1, -1, -1,  0,  1 };
    static NSUInteger EntryDirection[kNeighbors] = {        // translates the neighboring pixel hit into an entry direction
        kRightNeighbor,             // above
        kRightNeighbor,             // upper right
        kDownNeighbor,              // right
        kDownNeighbor,              // lower right
        kLeftNeighbor,              // below
        kLeftNeighbor,              // lower left
        kUpNeighbor,                // left
        kUpNeighbor };              // upper left
    #define OppositeDirection(DIRECTION)    ((DIRECTION+4)&0x07)    // macro to calculate the opposite neighbor position or direction
    
    - (NSInteger)edgeIndentForSize:(IconSizeSelector)selector
    {
        // the inset of black pixels from any transparent pixel for a given icon size
        // right now it's aribrarily the pixel size selector, so mini icons get no
        //  indent, large get 1 pixel, huge gets 2 pixels, ...
        return (NSInteger)selector;
    }
    
    - (NSBezierPath*)outlineFromBuffer:(SystemIconBuffer*)buffer forSize:(IconSizeSelector)selector
    {
        // Get the array of RGBA pixels for the source image
        // Do this before locking drawBufferLock becuase -dataForSize might need to convert an NSImage into
        //  a pixel buffer, and it will need IconFrameBuffer() to do that
        const UInt8* srcBuffer = [buffer dataForSize:selector].bytes;
    
        // Aquire a temporary frame buffer to use for the calculations
        OSSpinLockLock(&drawBufferLock);
        UInt8* pixels = IconFrameBuffer(selector);
        bzero(pixels,IconRGBABufferSize(selector));     // fill buffer with zeros
    
        NSInteger dim = IconPixelIntegerSize[selector];
    
        // Determine the "black" pixels of the image. This is done by setting all of the pixels to "black" (true) and then erasing
        //  a range of them near any transparent pixels in the source image. The size of the range is determined by edgeIndent.
        //  When edgeIndent is non-zero, the black pixels are required to be a least that many pixels away from any transparent
        //  pixel, effectively insetting the image from its edges.
        NSInteger edgeIdent = [self edgeIndentForSize:selector];
        // Paint the whole pixel map "black"
        FillSquareOfValues(0,0,dim,1,kBlackOffset,pixels,dim);
        // Scan all of the pixels (including one row & column of phantom pixels beyond the edge of the image) looking for
        //  transparent pixels. If found, erase the black pixel at that coordinate plus all pixels within edgeIndent of it.
        for ( NSInteger x=-1; x<=dim; x++ )
            for (NSInteger y=-1; y<=dim; y++ )
                if (GetValue(x,y,kAlphaOffset,srcBuffer,dim)<=kIsBodyAlphaThreshold)
                    // fill with not-black values at x,y plus edgeIndent pixels adjacent to it
                    FillSquareOfValues(x-edgeIdent,y-edgeIdent,edgeIdent*2+1,0,kBlackOffset,pixels,dim);
    
        // Find the starting pixel
        NSInteger startX = -1;
        NSInteger startY = -1;
        // Scanning left to right, bottom to top, find the first lower-left(ish) pixel
        for ( NSInteger x=0; x<dim; x++ )
            for ( NSInteger y=0; y<dim; y++ )
                if (GetValue(x, y, kBlackOffset, pixels, dim)!=0)
                    {
                    startX = x;
                    startY = y;
                    x = y = dim;    // break both loops
                    }
    
        // Create the path and set the start point
        NSBezierPath* path = [NSBezierPath bezierPath];
        if (startX<0 && startY<0)
            goto bail;              // there are no opaque pixels: return an empty path
        [path moveToPoint:NSMakePoint(startX+0.5,startY+0.5)];
    
        // Determine initial entry direction for first pixel
        // Conceptually, we'd want to walk around this pixel counter-clockwise to find the next-to-the-last pixel
        //  in the outline, which will tell us the direction the last edge pixel will (re)enter this one.
        // However ...
        // Because we "snuck" up on the first pixel by scanning columns left to right, we know that there are no
        //  black pixels immeidately below it or to its left. The only variable is whether there are pixels to its right and
        //  whether they are above or below it. Ultimately, we only need to test one configuration. If there
        //  are no pixels to the immediate right or lower-right, then the final entry direction will be from
        //  the right (6). If there are pixels in either of these locations, the final entry direction will be
        //  from the bottom (0).
        NSUInteger initialEntryDirection = 0;
        if (GetValue(startX+1,startY-1,kBlackOffset,pixels,dim)==0 && GetValue(startX+1,startY,kBlackOffset,pixels,dim)==0)
            initialEntryDirection = 6;
    
        // Start the outline trace
        // At each pixel, starting with the pixel in the opposite direction of the entry direction, test the pixels,
        //  clockwise, until we find the next black pixel adjacent to this one. Add it to the outline and repeat
        //  until we encounter the first pixel again, entered from the same direction (Jacob's stopping criterion).
        NSInteger x = startX;
        NSInteger lastX = x;
        NSInteger y = startY;
        NSInteger lastY = y;
        NSUInteger direction = initialEntryDirection;
        do {
            // Search, clockwise, for the next neighboring black pixel
            NSInteger nextX, nextY;
            NSUInteger nextDir = OppositeDirection(direction);
            do {
                // Progress to the next neighbor direction (note that when first entering the loop we always
                //  know that the pixel we entered from must be white, or we couldn't have entered from that direction,
                //  so the entry pixel never needs to be tested)
                nextDir += 1;
                if (nextDir>kNeighbors*2)
                    // Safety check: an image with a single, isolated, pixel will cause this loop to run forever
                    goto bail;
                nextX = x+NeighborX[nextDir&0x07];
                nextY = y+NeighborY[nextDir&0x07];
            } while (GetValue(nextX,nextY,kBlackOffset,pixels,dim)==0);
            // Loop exits with [nextX,nextY] of next clockwise black pixel and the direction of that pixel relative to this one
            x = nextX;                              // move to this point
            y = nextY;
            direction = EntryDirection[nextDir&0x07];   // translate pixel relative direction into entry direction
    
            if (x!=lastX || y!=lastY)
                {
                // This is a new point in the list: add it to the path
                [path lineToPoint:NSMakePoint(x+0.5,y+0.5)];
                lastX = x;
                lastY = y;
                }
        } while (x!=startX || y!=startY || direction!=initialEntryDirection);
        // Loop will exit when the countour has been traced back to its orignal point
    bail:
        [path closePath];
    
        OSSpinLockUnlock(&drawBufferLock);
        return path;
    }
    
    static NSUInteger GetValue( NSInteger x, NSInteger y, NSUInteger offset, const UInt8* buffer, NSInteger dim )
    {
        if (x>=0 && x<dim && y>=0 && y<dim )
            return buffer[((dim-y-1)*dim+x)*4+offset];
        return 0;
    }
    
    static void FillSquareOfValues( NSInteger x, NSInteger y, NSInteger size, NSUInteger value, NSUInteger offset, UInt8* buffer, NSInteger dim )
    {
        NSInteger endX = MIN(x+size,dim);
        NSInteger endY = MIN(y+size,dim);
        if (x<0) x = 0;
        if (y<0) y = 0;
        for ( NSInteger i=x; i<endX; i++ )
            for ( NSInteger j=y; j<endY; j++ )
                buffer[((dim-j-1)*dim+i)*4+offset] = value;
    }
    

    最后一个注意事项:得到的贝塞尔路径是 非常有效,因为它包含数百个(如果不是数千个)细线段。在我的代码中,我使用生成的路径将大纲(一次)绘制到屏幕外的缓冲区中,然后将缓冲区转换为可重用的缓存图像。

    从中创造一些东西,然后画出来,就像他们说的,留给学生作为练习…

        2
  •  0
  •   Ramkumar Paulraj Suresh    8 年前
    1. https://github.com/Chintan-Dave/UIImageScanlineFloodfill -你可以用这个算法来解决这个问题。

    2. 你得用这个 https://github.com/BradLarson/GPUImage 根据您的要求修改明暗器。

    推荐文章