CGContext:如何擦除位图上下文之外的像素(例如 kCGBlendModeClear)?

Posted

技术标签:

【中文标题】CGContext:如何擦除位图上下文之外的像素(例如 kCGBlendModeClear)?【英文标题】:CGContext: how do I erase pixels (e.g. kCGBlendModeClear) outside of a bitmap context? 【发布时间】:2013-11-09 14:04:03 【问题描述】:

我正在尝试使用 Core Graphics 构建橡皮擦工具,但我发现制作高性能橡皮擦非常困难 - 这一切都归结为:

CGContextSetBlendMode(context, kCGBlendModeClear)

如果您在 Google 上搜索如何使用 Core Graphics 进行“擦除”,几乎每个答案都会返回该 sn-p。问题是它仅(显然)在位图上下文中有效。如果您正在尝试实现交互式擦除,我看不出 kCGBlendModeClear 对您有何帮助 - 据我所知,您或多或少被锁定在屏幕上和屏幕外擦除 UIImage/CGImage并在著名的性能不佳的[UIView drawRect] 中绘制该图像。

这是我能做到的最好的:

-(void)drawRect:(CGRect)rect

    if (drawingStroke) 
        if (eraseModeOn) 
            UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, 0.0);
            CGContextRef context = UIGraphicsGetCurrentContext();
            [eraseImage drawAtPoint:CGPointZero];
            CGContextAddPath(context, currentPath);
            CGContextSetLineCap(context, kCGLineCapRound);
            CGContextSetLineWidth(context, lineWidth);
            CGContextSetBlendMode(context, kCGBlendModeClear);
            CGContextSetLineWidth(context, ERASE_WIDTH);
            CGContextStrokePath(context);
            curImage = UIGraphicsGetImageFromCurrentImageContext();
            UIGraphicsEndImageContext();
            [curImage drawAtPoint:CGPointZero];
         else 
            [curImage drawAtPoint:CGPointZero];
            CGContextRef context = UIGraphicsGetCurrentContext();
            CGContextAddPath(context, currentPath);
            CGContextSetLineCap(context, kCGLineCapRound);
            CGContextSetLineWidth(context, lineWidth);
            CGContextSetBlendMode(context, kCGBlendModeNormal);
            CGContextSetStrokeColorWithColor(context, lineColor.CGColor);
            CGContextStrokePath(context);
        
     else 
        [curImage drawAtPoint:CGPointZero];
    

绘制法线 (!eraseModeOn) 的性能可以接受;我正在将我的屏幕外绘图缓冲区(curImage,其中包含所有以前绘制的笔划)传送到当前的CGContext,并且我正在渲染当前正在绘制的线(路径)。它并不完美,但嘿,它有效,而且性能相当不错。

但是,因为kCGBlendModeNormal 显然不能在位图上下文之外工作,所以我不得不:

    创建位图上下文 (UIGraphicsBeginImageContextWithOptions)。 绘制我的屏幕外缓冲区(eraseImage,它实际上是从打开橡皮擦工具时的 curImage 派生的 - 所以实际上与 curImage 几乎相同,因为参数的缘故。 将当前正在绘制的“擦除线”(路径)渲染到位图上下文(使用kCGBlendModeClear 清除像素)。 将整个图像提取到屏幕外缓冲区 (curImage = UIGraphicsGetImageFromCurrentImageContext();) 最后将屏幕外缓冲区blit到视图的CGContext

这太可怕了,就性能而言。使用 Instrument 的 Time 工具,很明显这种方法的问题在哪里:

UIGraphicsBeginImageContextWithOptions 很贵 绘制屏幕外缓冲区两次代价高昂 将整个图像提取到屏幕外缓冲区的开销很大

自然而然,代码在真正的 iPad 上执行得非常糟糕。

我不太确定在这里做什么。我一直试图弄清楚如何在非位图上下文中清除像素,但据我所知,依赖kCGBlendModeClear 是一个死胡同。

有什么想法或建议吗?其他 ios 绘图应用如何处理擦除?


其他信息

我一直在使用CGLayer 方法,因为根据我所做的一些谷歌搜索,CGContextSetBlendMode(context, kCGBlendModeClear) 似乎可以在CGLayer 中工作。

但是,我对这种方法能否成功并不抱太大希望。在drawRect 中绘制图层(即使使用setNeedsDisplayInRect)是非常糟糕的; Core Graphics 正在阻塞将渲染层中的每个路径CGContextDrawLayerAtPoint(根据 Instruments)。据我所知,就性能而言,在这里使用位图上下文绝对是更可取的 - 当然,唯一的问题是上述问题(kCGBlendModeClear 在我将位图上下文blit到主要CGContext 后不起作用drawRect)。

【问题讨论】:

【参考方案1】:

我已经通过使用以下代码获得了很好的结果:

- (void)drawRect:(CGRect)rect

    if (drawingStroke) 
        if (eraseModeOn) 
            CGContextRef context = UIGraphicsGetCurrentContext();
            CGContextBeginTransparencyLayer(context, NULL);
            [eraseImage drawAtPoint:CGPointZero];

            CGContextAddPath(context, currentPath);
            CGContextSetLineCap(context, kCGLineCapRound);
            CGContextSetLineWidth(context, ERASE_WIDTH);
            CGContextSetBlendMode(context, kCGBlendModeClear);
            CGContextSetStrokeColorWithColor(context, [[UIColor clearColor] CGColor]);
            CGContextStrokePath(context);
            CGContextEndTransparencyLayer(context);
         else 
            [curImage drawAtPoint:CGPointZero];
            CGContextRef context = UIGraphicsGetCurrentContext();
            CGContextAddPath(context, currentPath);
            CGContextSetLineCap(context, kCGLineCapRound);
            CGContextSetLineWidth(context, self.lineWidth);
            CGContextSetBlendMode(context, kCGBlendModeNormal);
            CGContextSetStrokeColorWithColor(context, self.lineColor.CGColor);
            CGContextStrokePath(context);
        
     else 
        [curImage drawAtPoint:CGPointZero];
    

    self.empty = NO;

诀窍是将以下内容包装到 CGContextBeginTransparencyLayer / CGContextEndTransparencyLayer 调用中:

将擦除背景图像与上下文关联 在擦除背景图像上绘制“擦除”路径,使用kCGBlendModeClear

由于擦除背景图片的像素数据和擦除路径在同一层,所以具有清除像素的效果。

【讨论】:

这是关键行,CGContextSetBlendMode(context, kCGBlendModeClear);【参考方案2】:

遵循绘画范例的二维图形。绘画时,很难去除已经放在画布上的颜料,但在顶部添加更多颜料却非常容易。带有位图上下文的混合模式为您提供了一种用几行代码来做一些困难的事情(从画布上刮掉油漆)的方法。几行代码并不能使它成为一个简单的计算操作(这就是它执行缓慢的原因)。

无需进行屏幕外位图缓冲即可伪造清除像素的最简单方法是在图像上绘制视图的背景。

-(void)drawRect:(CGRect)rect

    if (drawingStroke) 
        CGColor lineCgColor = lineColor.CGColor;
        if (eraseModeOn) 
            //Use concrete background color to display erasing. You could use the backgroundColor property of the view, or define a color here
            lineCgColor = [[self backgroundColor] CGColor];
         
        [curImage drawAtPoint:CGPointZero];
        CGContextRef context = UIGraphicsGetCurrentContext();
        CGContextAddPath(context, currentPath);
        CGContextSetLineCap(context, kCGLineCapRound);
        CGContextSetLineWidth(context, lineWidth);
        CGContextSetBlendMode(context, kCGBlendModeNormal);
        CGContextSetStrokeColorWithColor(context, lineCgColor);
        CGContextStrokePath(context);
     else 
        [curImage drawAtPoint:CGPointZero];
    

更困难(但更正确)的方法是在后台串行队列上进行图像编辑以响应编辑事件。当你得到一个新动作时,你在后台对图像缓冲区进行位图渲染。当缓冲图像准备好时,您调用setNeedsDisplay 以允许在下一个更新周期重绘视图。这更正确,因为drawRect: 应该尽快显示您的视图内容,而不是处理编辑操作。

@interface ImageEditor : UIView

@property (nonatomic, strong) UIImage * imageBuffer;
@property (nonatomic, strong) dispatch_queue_t serialQueue;
@end

@implementation ImageEditor

- (dispatch_queue_t) serialQueue

    if (_serialQueue == nil)
    
        _serialQueue = dispatch_queue_create("com.example.com.imagebuffer", DISPATCH_QUEUE_SERIAL);
    
    return _serialQueue;


- (void)editingAction

    dispatch_async(self.serialQueue, ^
        CGSize bufferSize = [self.imageBuffer size];

        UIGraphicsBeginImageContext(bufferSize);

        CGContext context = UIGraphicsGetCurrentContext();

        CGContextDrawImage(context, CGRectMake(0, 0, bufferSize.width, bufferSize.height), [self.imageBuffer CGImage]);

        //Do editing action, draw a clear line, solid line, etc

        self.imageBuffer = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();

        dispatch_async(dispatch_get_main_queue(), ^
            [self setNeedsDisplay];
        );
    );

-(void)drawRect:(CGRect)rect

    [self.imageBuffer drawAtPoint:CGPointZero];


@end

【讨论】:

所以我之前已经尝试过了,但运气不佳([UIColor clearColor]') - though my blend mode setting was kCBBlendModeClear... I didn't try kCGBlendModeNormal`)。我会试一试,让你知道! 更新 - 不起作用 - 它不会清除在 [curImage drawAtPoint:CGPointZero] 调用中绘制的像素。 这里的所有 ivars 都很难说出你做错了什么。其他代码可能将 lineWidth 设置为 0.0 或 currentPath 可能未初始化。基本要点是一样的,只是用清晰(或背景)颜色画线。 如果我取出 ivars 并明确设置这些值,我会得到相同的结果 - 没有清除像素。 currentPath 肯定已初始化,因为如果我使用不同的颜色(例如 [UIColor greenColor]),我可以看到它。问题似乎是我没有在位图上下文中绘图 - 我只是使用 drawRectUIGraphicsGetCurrentContext() 返回的默认上下文 - 这就是我的问题:-( 您在不透明背景之上绘制透明度,实际上并未改变图像。当您使用 clearColor 时,您会“看穿”绘制的线条。您必须使用具体颜色定义视图中背景颜色的外观并绘制它。更新答案【参考方案3】:

key 是 CGContextBeginTransparencyLayer 并使用 clearColor 并设置 CGContextSetBlendMode(context, kCGBlendModeClear);

【讨论】:

以上是关于CGContext:如何擦除位图上下文之外的像素(例如 kCGBlendModeClear)?的主要内容,如果未能解决你的问题,请参考以下文章

使用 CGContext 绘制后擦除

如何在 C++ 中绘制纹理/位图的区域?

如何更改图像中的像素

如何检查 CGContext 是不是包含点?

使用 CGContext 调整 UIImage 的大小会使我的图像变软,像素密度错误

使用 CGContext 缩放到小尺寸时,CGImage 的像素数据不完整