使用具有图像全分辨率的 bezierpath 遮蔽图像

Masking an image using bezierpath with image's full resolution

嗨,我有一条路径(形状)和一张高分辨率的图像。我在绘制路径的视图中将高分辨率图像设为 AspectFit,我想用路径遮盖图像,但要以图像的全分辨率,而不是我们看到路径的分辨率。问题是,当我不为高分辨率遮罩放大它们时,它工作得很好,但当我这样做时,一切都搞砸了。面具被拉长,起源没有意义。

我想要的只是能够以相同的图像纵横比(在图像的全分辨率下)放大路径并正确定位它,以便它可以正确地遮盖高分辨率图像。 我试过这个:

Masking CGContext with a CGPathRef?

还有这个

Creating mask with CGImageMaskCreate is all black (iphone)

还有这个

none 当我尝试屏蔽高质量图像(大于屏幕分辨率)时,其中的工作正常

编辑 我在 github 上发布了一个工作项目来展示正常质量遮罩(在屏幕分辨率下)和高质量遮罩(在图像分辨率下)之间的问题。我真的很感激任何帮助。 https://github.com/Reza-Abdolahi/HighResMasking

如果我正确理解你的问题:

  • 您的图像视图包含可能已使用 UIViewContentModeScaleAspectFit 缩小(甚至放大)的图像。
  • 您有一条贝塞尔曲线路径,其点位于该图像视图的几何(坐标系)中。

现在您想以原始分辨率创建图像的副本,并用贝塞尔曲线路径遮盖。

我们可以认为图像有自己的几何形状,原点在图像的左上角,沿每个轴的一个单位是一个点。那么我们需要做的是:

  1. 创建一个足够大的图形渲染器,以便在不缩放的情况下绘制图像。此渲染器的几何形状是图像的几何形状。
  2. 将贝塞尔曲线路径从视图几何图形转换为渲染器几何图形。
  3. 将转换后的路径应用于渲染器的剪辑区域。
  4. 将图像(未转换)绘制到渲染器中。

第 2 步是困难的一步,因为我们必须想出正确的 CGAffineTransform。在 aspect-fit 场景中,变换不仅需要缩放图像,还可能沿 x 轴或 y 轴(但不是两者)平移图像。但让我们更通用,支持其他 UIViewContentMode 设置。这是一个类别,可让您请求 UIImageView 进行转换,将视图几何中的点转换为图像几何中的点:

@implementation UIImageView (ImageGeometry)

/**
 * Return a transform that converts points in my geometry to points in the
 * image's geometry. The origin of the image's geometry is at its upper
 * left corner, and one unit along each axis is one point in the image.
 */
- (CGAffineTransform)imageGeometryTransform {
    CGRect viewBounds = self.bounds;
    CGSize viewSize = viewBounds.size;
    CGSize imageSize = self.image.size;

    CGFloat xScale = imageSize.width / viewSize.width;
    CGFloat yScale = imageSize.height / viewSize.height;
    CGFloat tx, ty;
    switch (self.contentMode) {
        case UIViewContentModeScaleToFill: tx = 0; ty = 0; break;
        case UIViewContentModeScaleAspectFit:
            if (xScale > yScale) { tx = 0; ty = 0.5; yScale = xScale; }
            else if (xScale < yScale) { tx = 0.5; ty = 0; xScale = yScale; }
            else { tx = 0; ty = 0; }
            break;
        case UIViewContentModeScaleAspectFill:
            if (xScale < yScale) { tx = 0; ty = 0.5; yScale = xScale; }
            else if (xScale > yScale) { tx = 0.5; ty = 0; xScale = yScale; }
            else { tx = 0; ty = 0; imageSize = viewSize; }
            break;
        case UIViewContentModeCenter: tx = 0.5; ty = 0.5; xScale = yScale = 1; break;
        case UIViewContentModeTop: tx = 0.5; ty = 0; xScale = yScale = 1; break;
        case UIViewContentModeBottom: tx = 0.5; ty = 1; xScale = yScale = 1; break;
        case UIViewContentModeLeft: tx = 0; ty = 0.5; xScale = yScale = 1; break;
        case UIViewContentModeRight: tx = 1; ty = 0.5; xScale = yScale = 1; break;
        case UIViewContentModeTopLeft: tx = 0; ty = 0; xScale = yScale = 1; break;
        case UIViewContentModeTopRight: tx = 1; ty = 0; xScale = yScale = 1; break;
        case UIViewContentModeBottomLeft: tx = 0; ty = 1; xScale = yScale = 1; break;
        case UIViewContentModeBottomRight: tx = 1; ty = 1; xScale = yScale = 1; break;
        default: return CGAffineTransformIdentity; // Mode not supported by UIImageView.
    }

    tx *= (imageSize.width - xScale * (viewBounds.origin.x + viewSize.width));
    ty *= (imageSize.height - yScale * (viewBounds.origin.y + viewSize.height));
    CGAffineTransform transform = CGAffineTransformMakeTranslation(tx, ty);
    transform = CGAffineTransformScale(transform, xScale, yScale);
    return transform;
}

@end

有了这个,我们就可以编写屏蔽图像的代码了。在我的测试应用程序中,我有一个名为 PathEditingViewUIImageView 子类,用于处理贝塞尔曲线路径编辑。所以我的视图控制器像这样创建蒙版图像:

- (UIImage *)maskedImage {
    UIImage *image = self.pathEditingView.image;
    UIGraphicsImageRendererFormat *format = [[UIGraphicsImageRendererFormat alloc] init];
    format.scale = image.scale;
    format.prefersExtendedRange = image.imageRendererFormat.prefersExtendedRange;
    format.opaque = NO;
    UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:image.size format:format];
    return [renderer imageWithActions:^(UIGraphicsImageRendererContext * _Nonnull rendererContext) {
        UIBezierPath *path = [self.pathEditingView.path copy];
        [path applyTransform:self.pathEditingView.imageGeometryTransform];
        CGContextRef gc = UIGraphicsGetCurrentContext();
        CGContextAddPath(gc, path.CGPath);
        CGContextClip(gc);
        [image drawAtPoint:CGPointZero];
    }];
}

看起来像这样:

当然很难说输出图像是全分辨率的。让我们通过将输出图像裁剪到贝塞尔曲线路径的边界框来解决这个问题:

- (UIImage *)maskedAndCroppedImage {
    UIImage *image = self.pathEditingView.image;
    UIBezierPath *path = [self.pathEditingView.path copy];
    [path applyTransform:self.pathEditingView.imageGeometryTransform];
    CGRect pathBounds = CGPathGetPathBoundingBox(path.CGPath);
    UIGraphicsImageRendererFormat *format = [[UIGraphicsImageRendererFormat alloc] init];
    format.scale = image.scale;
    format.prefersExtendedRange = image.imageRendererFormat.prefersExtendedRange;
    format.opaque = NO;
    UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:pathBounds.size format:format];
    return [renderer imageWithActions:^(UIGraphicsImageRendererContext * _Nonnull rendererContext) {
        CGContextRef gc = UIGraphicsGetCurrentContext();
        CGContextTranslateCTM(gc, -pathBounds.origin.x, -pathBounds.origin.y);
        CGContextAddPath(gc, path.CGPath);
        CGContextClip(gc);
        [image drawAtPoint:CGPointZero];
    }];
}

遮罩和裁剪看起来像这样:

您可以在此演示中看到输出图像比输入视图中可见的细节多得多,因为它是在输入图像的全分辨率下生成的。

作为次要答案,我让它与这段代码一起工作,为了更好地理解,您也可以在 github 上获取工作项目,看看它是否适用于所有情况。 我的 github 项目: https://github.com/Reza-Abdolahi/HighResMasking

解决问题的部分代码:

-(UIImage*)highResolutionMasking{
    NSLog(@"///High quality (Image resolution) masking///////////////////////////////////////////////////");

    //1.Rendering the path into an image with the size of _targetBound (which is the size of a device screen sized view in which the path is drawn.)
    CGFloat aspectRatioOfImageBasedOnHeight = _highResolutionImage.size.height/ _highResolutionImage.size.width;
    CGFloat aspectRatioOfTargetBoundBasedOnHeight = _targetBound.size.height/ _targetBound.size.width;

    CGFloat pathScalingFactor = 0;
    if ((_highResolutionImage.size.height >= _targetBound.size.height)||
        (_highResolutionImage.size.width  >= _targetBound.size.width)) {
            //Then image is bigger than targetBound

            if ((_highResolutionImage.size.height<=_highResolutionImage.size.width)) {
            //The image is Horizontal

                CGFloat newWidthForTargetBound =_highResolutionImage.size.width;
                CGFloat ratioOfHighresImgWidthToTargetBoundWidth = (_highResolutionImage.size.width/_targetBound.size.width);
                CGFloat newHeightForTargetBound = _targetBound.size.height* ratioOfHighresImgWidthToTargetBoundWidth;

                _bigTargetBound = CGRectMake(0, 0, newWidthForTargetBound, newHeightForTargetBound);
                pathScalingFactor = _highResolutionImage.size.width/_targetBound.size.width;

            }else if((_highResolutionImage.size.height > _highResolutionImage.size.width)&&
                     (aspectRatioOfImageBasedOnHeight  <= aspectRatioOfTargetBoundBasedOnHeight)){
                //The image is Vertical but has smaller aspect ratio (based on height) than targetBound

                CGFloat newWidthForTargetBound =_highResolutionImage.size.width;
                CGFloat ratioOfHighresImgWidthToTargetBoundWidth = (_highResolutionImage.size.width/_targetBound.size.width);
                CGFloat newHeightForTargetBound = _targetBound.size.height* ratioOfHighresImgWidthToTargetBoundWidth;

                _bigTargetBound = CGRectMake(0, 0, newWidthForTargetBound, newHeightForTargetBound);
                pathScalingFactor = _highResolutionImage.size.width/_targetBound.size.width;

            }else if((_highResolutionImage.size.height > _highResolutionImage.size.width)&&
                     (aspectRatioOfImageBasedOnHeight  > aspectRatioOfTargetBoundBasedOnHeight)){

                CGFloat newHeightForTargetBound =_highResolutionImage.size.height;
                CGFloat ratioOfHighresImgHeightToTargetBoundHeight = (_highResolutionImage.size.height/_targetBound.size.height);
                CGFloat newWidthForTargetBound = _targetBound.size.width* ratioOfHighresImgHeightToTargetBoundHeight;

                _bigTargetBound = CGRectMake(0, 0, newWidthForTargetBound, newHeightForTargetBound);
                pathScalingFactor = _highResolutionImage.size.height/_targetBound.size.height;
            }else{
                //Do nothing
            }
    }else{
            //Then image is smaller than targetBound
            _bigTargetBound = _imageRect;
            pathScalingFactor =1;
    }

    CGSize correctedSize = CGSizeMake(_highResolutionImage.size.width  *_scale,
                                      _highResolutionImage.size.height *_scale);

    _bigImageRect= AVMakeRectWithAspectRatioInsideRect(correctedSize,_bigTargetBound);

    //Scaling path
    CGAffineTransform scaleTransform = CGAffineTransformIdentity;
    scaleTransform = CGAffineTransformScale(scaleTransform, pathScalingFactor, pathScalingFactor);

    CGPathRef scaledCGPath = CGPathCreateCopyByTransformingPath(_examplePath.CGPath,&scaleTransform);

    //Render scaled path into image
    UIGraphicsBeginImageContextWithOptions(_bigTargetBound.size, NO, 1.0);
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextAddPath (context, scaledCGPath);
    CGContextSetFillColorWithColor (context, [UIColor redColor].CGColor);
    CGContextSetStrokeColorWithColor (context, [UIColor redColor].CGColor);
    CGContextDrawPath (context, kCGPathFillStroke);
    UIImage * pathImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    NSLog(@"High res pathImage.size: %@",NSStringFromCGSize(pathImage.size));

    //Cropping it from targetBound into imageRect
    _maskImage = [self cropThisImage:pathImage toRect:_bigImageRect];
    NSLog(@"High res _croppedRenderedPathImage.size: %@",NSStringFromCGSize(_maskImage.size));

    //Masking the high res image with my mask image which both have the same size now.
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGImageRef maskImageRef = [_maskImage CGImage];
    CGContextRef myContext = CGBitmapContextCreate (NULL, _highResolutionImage.size.width, _highResolutionImage.size.height, 8, 0, colorSpace, kCGImageAlphaPremultipliedLast);
    CGColorSpaceRelease(colorSpace);
    if (myContext==NULL)
        return NULL;

    CGFloat ratio = 0;
    ratio = _maskImage.size.width/ _highResolutionImage.size.width;
    if(ratio * _highResolutionImage.size.height < _maskImage.size.height) {
        ratio = _maskImage.size.height/ _highResolutionImage.size.height;
    }

    CGRect rectForMask  = {{0, 0}, {_maskImage.size.width, _maskImage.size.height}};
    CGRect rectForImageDrawing  = {{-((_highResolutionImage.size.width*ratio)-_maskImage.size.width)/2 , -((_highResolutionImage.size.height*ratio)-_maskImage.size.height)/2},
        {_highResolutionImage.size.width*ratio, _highResolutionImage.size.height*ratio}};

    CGContextClipToMask(myContext, rectForMask, maskImageRef);
    CGContextDrawImage(myContext, rectForImageDrawing, _highResolutionImage.CGImage);
    CGImageRef newImage = CGBitmapContextCreateImage(myContext);
    CGContextRelease(myContext);
    UIImage *theImage = [UIImage imageWithCGImage:newImage];
    CGImageRelease(newImage);
    return theImage;
}

-(UIImage *)cropThisImage:(UIImage*)image toRect:(CGRect)rect{
    CGImageRef subImage = CGImageCreateWithImageInRect(image.CGImage, rect);
    UIImage *croppedImage = [UIImage imageWithCGImage:subImage];
    CGImageRelease(subImage);
    return croppedImage;
}