选择相似的像素,在像素周围绘制贝塞尔路径并填充颜色 - iOS



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

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

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

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

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

我的解决方案基于一个名为SystemIconBuffer的结构,该结构包含图标的多种分辨率。你基本上可以忽略所有这些。重要的步骤是将您的图像栅格化为RGBA字节的数组(我使用CGBitmatContextCreate(,并在srcBuffer中获取指向该数组的指针。

最后,我的图像总是正方形的,所以我的代码只有一个维度值(dim(,你需要将其转换为dimXdimY,用于矩形图像。

再说一次,我的代码只对找到图像的透明边缘感兴趣,所以它只查看每个像素的"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;
}

最后一点:生成的贝塞尔路径效率高,因为它包含数百甚至数千条微小的线段。在我的代码中,我使用生成的路径将大纲(一次(绘制到屏幕外缓冲区中,然后将其转换为我重复使用的缓存图像。

正如他们所说,从中创造一些东西并绘制它被留给学生作为练习......

  1. https://github.com/Chintan-Dave/UIImageScanlineFloodfill - 您可以使用此算法来解决此问题。

  2. 您必须使用此 https://github.com/BradLarson/GPUImage 来根据您的要求修改着色器。

最新更新