65.9K
CodeProject 正在变化。 阅读更多。
Home

用于裁剪图片的视图类

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.81/5 (12投票s)

2016年1月12日

CPOL

12分钟阅读

viewsIcon

39705

downloadIcon

1041

本文介绍了自定义类 MMSCropImageView 的实现。该类具有在图像上绘制和移动矩形以识别裁剪区域的功能,并将其作为 UIImage 返回。文章解释了裁剪位图的注意事项和解决方案。

引言

也许您的应用程序与烹饪食谱、人物资料、车辆记录、队友或学生有关。您可能希望为它们添加照片。要实现此功能,您需要提供一套基本的图像处理操作。裁剪图像是一项基本操作。

我最近有此需求。像许多程序员一样,我进行了 Google 搜索,希望能从他人的经验中学习。我发现许多人曾为此问题苦苦挣扎,而他们的解决方案总不能让我完全满意。本文分享了我学到的知识,并详细介绍了如何实现一个名为 `MMSCropImageView` 的对象,您可以将其用于支持基本的图像裁剪功能。

背景

处理静态图像时,您会使用 `UIImage` 来显示它。`UIImage` 是 `UIImageView` 和底层位图之间的粘合剂。`UIImage` 将位图渲染到当前图形上下文,并配置变换属性、方向和缩放等。然而,这些操作都不会改变底层表示。它们控制位图如何渲染到图形上下文,但要实现对其的更改,您需要使用 *Core Graphics Framework*。

处理 `UIImage` 的位图可能会令人困惑,因为自然而然地会认为 `UIImage` 与位图是一一对应的。事实并非如此。屏幕上显示的内容通常是经过转换的。例如,`UIImage` 的宽度可能是 4288 像素,高度为 2848 像素,而位图的尺寸可能旋转了 90 度。

如果您在不考虑这些属性的情况下处理位图,您可能会对转换后生成的位图不符合预期感到困惑。裁剪就是其中一个用例。

当用户在图像上拖动一个矩形时,该矩形的方向是相对于 `UIImage` 和渲染到屏幕的变换而言的。因此,如果 `imageOrientation` 设置为 `UIImageOrientedUp` 以外的任何值,并且您在裁剪位图时不考虑此属性,则生成的图像将以不同的方式显示失真。

需要考虑的其他属性是图像相对于视图的尺寸。通常情况下,它们的尺寸是不同的。由于用户通过视图识别裁剪区域,因此必须将位图、视图和裁剪矩形之间的尺寸标准化,以匹配您在屏幕上看到的内容和从位图中提取的内容。

CropDemo 应用

本文的其余部分将介绍一个名为 `CropDemo` 的简单程序。我将使用它来演示如何使用 `MMSCropImageView` 和辅助类来裁剪图像。

该应用程序在屏幕顶部显示一张大图。用户用手指在图像上拖动以识别裁剪区域。当手指在图像上拖动时,会出现一个带有透明背景的白色矩形。要重新定位矩形,请用手指按住它并在图像周围移动。点击矩形外部可将其清除。要裁剪图像,请按屏幕底部的裁剪按钮,矩形覆盖的区域将显示在原始图像下方。

图 1 - 应用窗口

该应用程序包含一个类和一个类别供您在自己的应用程序中直接使用:`MMSCropImageView` 和 `UIImage+cropping`。

`MMSCropImageView` 是 `UIImageView` 的子类。它提供了在视图上拖动矩形、移动矩形、清除矩形以及返回从原始图像中提取的、由裁剪区域标识的 `UIImage` 的功能。

名为 `UIImage+cropping` 的类别为 `UIImage` 类添加了裁剪位图的方法。该类别可以独立于 `MMSCropImageView` 类使用。

裁剪 UIImage

`UIImage+cropping` 类别实现了一个 `public` 方法来裁剪位图并将其作为 `UIImage` 返回。

-(UIImage*)cropRectangle:(CGRect)cropRect inFrame:(CGSize)frameSize 

参数 `cropRect` 是要从图像中剪切的矩形区域。其尺寸相对于第二个参数 `frameSize`。

参数 `frameSize` 包含渲染图像的视图的尺寸。

由于用户的输入都相对于视图,因此有必要标准化视图和图像之间的尺寸。所采用的方法是将位图调整为视图的尺寸。

需要考虑的另一个变量是图像的方向。必须确定方向才能将裁剪矩形定位到位图上,并根据图像渲染与位图的方向可能不同来缩放尺寸以匹配视图。

缩放位图的步骤

以下是缩放 `UIImage` 的底层位图的步骤。

一、检查 `imageOrientation`,如果图像向左或向右方向,则交换缩放尺寸的 `height` 和 `width`。

if (self.imageOrientation == UIImageOrientationLeft || self.imageOrientation == UIImageOrientationRight) {
            
   scaleSize = CGSizeMake(round(scaleSize.height), round(scaleSize.width));

}

二、在缩放尺寸中创建一个位图上下文。

CGContextRef context = CGBitmapContextCreate(nil, scaleSize.width, scaleSize.height, 
CGImageGetBitsPerComponent(self.CGImage), CGImageGetBytesPerRow(self.CGImage)/CGImageGetWidth
(self.CGImage)*scaleSize.width, CGImageGetColorSpace(self.CGImage), CGImageGetBitmapInfo(self.CGImage));

三、将位图绘制到新上下文中。

CGContextDrawImage(context, CGRectMake(0, 0, scaleSize.width, scaleSize.height), self.CGImage);

四、从位图上下文中获取一个 `CGImageRef`。

CGImageRef imgRef = CGBitmapContextCreateImage(context);

五、从第四步返回的 `CGImageRef` 实例化一个 `UIImage`。

UIImage* returnImg = [UIImage imageWithCGImage:imgRef];

变量 `returnImg` 的 `height` 和 `width` 等于缩放尺寸。下载附件代码,在文件 *UIImage+cropping.m* 中查看 `scaleBitmapToSize:` 的完整实现。

转置裁剪矩形

裁剪矩形的起点和大小是相对于视图而言的。因此,为了正确裁剪位图,必须转置该矩形,使其基于方向属性进行定位。方法 `transposeCropRect:inDimension:forOrientation:` 将裁剪矩形转置到目标方向。参数 `inDimension` 是包围该矩形的框架的 `width` 和 `height`。

最好用图片来展示。以下图像描绘了当 `imageOrientation` 为左、右和下时必须发生的起点和大小的转换。蓝色方块代表矩形的原始起点,红色矩形代表转置后新矩形的位置。

图 2 - UIImageOrientationLeft 的裁剪矩形转换
 

图 3 - UIImageOrientationRight 的裁剪矩形
 

图 4 - UIImageOrientationDown 的裁剪矩形
 

由于 `UIImage` 和位图方向相同,`UIImageOrientationUp` 不需要转换。

以下代码展示了当方向为 `UIImageOrientationLeft` 时转换起点和大小的算法。

case UIImageOrientationLeft:
         transposedRect.origin.x = dimension.height - (cropRect.size.height + cropRect.origin.y);
         transposedRect.origin.y = cropRect.origin.x;
         transposedRect.size = CGSizeMake(cropRect.size.height, cropRect.size.width);
         break;

请参阅 *UIImage+cropping.m* 文件中所有可能方向的计算。

裁剪步骤

现在辅助方法已经解释清楚,裁剪 `UIImage` 就变得非常简单了。第一步是将位图缩放到框架尺寸。由于裁剪矩形的坐标空间存在于视图内,因此位图的空间必须与之匹配才能应用。另一种方法是将裁剪矩形缩放到位图的坐标空间。我选择了前者,因为它符合用户的视角。

UIImage* img = [self scaleBitmapToSize:frameSize];

接下来,从位图中提取裁剪区域。它调用 Core Graphics 函数 `CGImageCreateWithImageInRect` 并传入一个已转置的裁剪矩形。

CGImageRef cropRef = CGImageCreateWithImageInRect(img.CGImage, [self transposeCropRect:cropRect inDimension:frameSize forOrientation:self.imageOrientation]);

最后,它使用类工厂从裁剪后的位图中创建一个 `UIImage`,并将位图、比例因子 1.0 和源图像的方向作为参数传入。方向是关键,因为返回的图像会像其源一样显示。如果您始终使用像 `UIImageOrientationUp` 这样的常量,图像将显示为旋转、镜像或两者的组合,具体取决于原始值。

UIImage* croppedImg = [UIImage imageWithCGImage:cropRef scale:1.0 orientation:self.imageOrientation];

`cropRectangle:inFrame:` 的完整实现如下

/* cropRectangle:inFrame returns a new UIImage cut from the cropArea of the underlying image.  It first scales the underlying image to the scale size before cutting the crop area from it. The returned CGImage is in the dimensions of the cropArea and it is oriented the same as the underlying CGImage as is the imageOrientation.
 */
-(UIImage*)cropRectangle:(CGRect)cropRect inFrame:(CGSize)frameSize {

    frameSize = CGSizeMake(round(frameSize.width), round(frameSize.height));

    /* resize the image to match the zoomed content size
     */
    UIImage* img = [self scaleBitmapToSize:frameSize];

    /* crop the resized image to the crop rectangel.
     */
    CGImageRef cropRef = CGImageCreateWithImageInRect(img.CGImage, [self transposeCropRect:cropRect inDimension:frameSize forOrientation:self.imageOrientation]);

    UIImage* croppedImg = [UIImage imageWithCGImage:cropRef scale:1.0 orientation:self.imageOrientation];

    return croppedImg;

}

绘制裁剪矩形

为了识别裁剪矩形,用户会在图像上拖出一个矩形区域。绘制完成后,可以移动矩形以调整其起点。`MMSCropImageView` 类支持绘制和定位矩形以及返回裁剪图像的功能。它将这些功能封装在一个 `UIImageView` 的子类中。

为了绘制和清除裁剪矩形,将 `UIPanGestureRecognizer` 和 `UITapGestureRecognizer` 添加到视图中。这些手势会同时识别,因此必须通过实现 `UIGestureRecognizerDelegate` 方法 `gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:` 来启用识别。对于一对组合中的一个返回 `true`,但不能两个都返回 `true`。

-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer 
shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
   
    /* Enable to recognize the pan and tap gestures simultaneous for both the imageView and cropView
     */
    if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]] 
    && [otherGestureRecognizer isKindOfClass:[UITapGestureRecognizer class]]) {
        
        return YES;        
    }
    
    return NO;    
}

点击手势目标隐藏裁剪视图

/* hideCropRectangle: the crop view becomes hidden when the user taps outside the crop view frame.
 */
- (IBAction)hideCropRectangle:(UITapGestureRecognizer *)gesture {    
    
    if (!cropView.hidden) {
        
        cropView.hidden = true;
        
        cropView.frame = CGRectMake(-1.0, -1.0, 0.0, 0.0);        
    }    
}

为了更准确地识别手势的第一个触摸点,它使用了 `DragCropRectRecognizer` 类。它是 `UIPanGestureRecognizer` 的子类,用于精确确定裁剪矩形的起点。它重写了 `touchesBegan:withEvent:` 方法来保存手势的第一个触摸点。

选择这种方法来识别起点,而不是在处理状态 `UIGestureRecognizerStateBegan` 时设置它,因为我发现 `locationInView:` 返回的点与您的手指首次接触的点有偏移。

这是识别起点的代码

/* touchesBegan:withEvent: override the UIPanGestureRecognizer to identify the point 
that began the touch gesture.  When the action method is called and the gesture state 
is UIGestureRecognizerStateBegan, the point returned from locationInView is not the point 
that began the gesture. This routine sets the origin of the pan gesture to the first touch point.
 */

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    
    NSEnumerator* touchEnumerator = [touches objectEnumerator];
    
    UITouch* touch;
    
    while (touch = [touchEnumerator nextObject]) {
        if (touch.phase == UITouchPhaseBegan) {
            self.origin = [touch locationInView:self.view];
            break;
        };
    }
    
    [super touchesBegan:touches withEvent:event];
    
}

当用户继续在图像上移动手指时,平移手势会反复调用其目标 `drawRectangle`,并根据触摸的新位置绘制裁剪矩形。裁剪矩形是一个 `UIView`,并且是 `UIImageView` 的子视图,具有白色透明背景和实心白色边框。计算裁剪矩形的起点和大小的最重要因素是基于开始平移手势的点来计算它们。这一点被称为拖动起点。

计算裁剪矩形的第一步是确定新点相对于拖动起点的象限:左上(第一象限)、右上(第二象限)、右下(第三象限)或左下(第四象限)。确定后,计算新的起点和大小。

这是当当前点落在第三象限时计算裁剪矩形尺寸的代码。请参阅源代码文件 `MMSCropImageView.m` 中的 `drawRectangle:` 方法以获取所有计算。

cropRect = cropView.frame;
        
CGPoint currentPoint = [gesture locationInView:self];
        
if (currentPoint.x >= dragOrigin.x && currentPoint.y >= dragOrigin.y) {
            
   /* finger is dragged down and right with respect to the origin start (Quadrant III);  cropViews origin is the original touchpoint.
    */       
   cropRect.origin = dragOrigin;
            
   cropRect.size = CGSizeMake(currentPoint.x - cropRect.origin.x, currentPoint.y - cropRect.origin.y);         
}

移动裁剪矩形

一旦绘制出裁剪矩形,`MMSCropImageView` 类就允许用户在图像上重新定位它。它向裁剪矩形的 `UIView` 添加了一个 `UIPanGestureRecognizer` 来响应移动手势。当手势开始时,会记录两个点:触摸点(称为触摸起点)和裁剪矩形的起点(称为拖动起点)。它们在整个拖动操作中保持不变。

    If (gesture.state == UIGestureRecognizerStateBegan) {
        
        /*  save the crop view frame's origin to compute the changing position 
        as the finger glides around the screen.  Also, save the first touch point 
        compute the amount to change the frames origin.
         */
        
        touchOrigin = [gesture locationInView:self];
        
        dragOrigin = cropView.frame.origin;        
    }

当用户的手指在图像上移动时,它会计算从触摸起点开始的 `x` 和 `y` 的变化量。为了重新定位裁剪矩形,它通过将 `x` 和 `y` 的变化量添加到拖动起点中相应的变量来更新框架起点。所有计算都相对于这些起始点。

        CGFloat dx, dy;
        
        CGPoint currentPt = [gesture locationInView:self];
        
        dx = currentPt.x - touchOrigin.x;
        
        dy = currentPt.y - touchOrigin.y;
        
        cropView.frame = CGRectMake(dragOrigin.x + dx, dragOrigin.y + dy, cropView.frame.size.width, cropView.frame.size.height);

点击手势的特殊处理

手势会沿着子视图链向上查找处理程序,直到到达父视图。如果子视图不支持该手势而父视图支持,则父视图的处理程序将执行。如果操作对子视图无效,这可能会产生不利后果。

在此示例中,点击图像(在裁剪矩形外部)会将其移除。如果裁剪视图不处理点击手势,而用户点击了矩形内部,则父视图的处理程序将被调用并移除它。为了防止父视图处理程序执行,点击手势被添加到带有默认目标的裁剪视图中。

    // Create the swall gesture and attach it to the cropView.
    swallowGesture = [[UITapGestureRecognizer alloc] init];
    
    [cropView addGestureRecognizer:swallowGesture];

关注点

在对此类进行研究和开发的过程中,我花费了大量时间来解决从放大渲染中提取的裁剪图像与原始图像在清晰度方面的差异。尽管裁剪后的图像与放大后的裁剪区域具有相同的像素尺寸,但在屏幕上显示时却显得像素化。

我无法告诉你我花了多少无数个小时试图理解和解决裁剪图像与原始图像在清晰度方面的细微差别。当我最终确信自己从正确的起点、高度和宽度提取了图像,并且 `UIImageView` 的尺寸完全相同之后,我开始向别处寻找。这时我对自己说,让我看看它在实际设备上的显示效果,而不是在模拟器上。

果然!裁剪后的图像与放大后的原始图像具有相同的清晰度,没有算法更改。我断定这些差异是模拟器的产物。

既然我找到了解释,并且为此投入了过多的时间,所以我没有试图去理解模拟器为何会显示这种行为。如果任何读者有任何见解,请在评论中分享或通过电子邮件与我联系。正如您可能注意到的,它仍然困扰着我,但不足以让我进一步研究。

Using the Code

本示例的代码已附加到文章的 zip 文件中。您也可以在 GitHub 上找到它:https://github.com/miller-ms/MMSCropImageView

要在您自己的应用程序中使用该代码,请在您的 Storyboard 中为图像视图控件选择自定义类 `MMSCropImageView`。如果您不使用 Storyboard,请在计划显示图像的视图控制器中创建一个。

MMSCropImageView *cv = [[MMSCropImageView alloc] initWithFrame:CGRectMake(10, 10, 200, 100)];

在将要与该对象交互的文件中导入头文件。

#import <MMSCropImageView.h>

为显示该对象的视图的视图控制器添加一个事件处理程序。该事件处理程序很可能连接到一个按钮,用户通过该按钮启动裁剪操作。

- (IBAction)crop:(UIButton *)sender {

    UIImage* croppedImage; // = self.imageView.image;

    croppedImage =  [self.imageView crop];

    self.croppedView.image = croppedImage;
}

此示例只是将返回的图像显示在另一个 `UIImageView` 中。

摘要

希望本文能揭开图像裁剪的复杂性。

此方法通过将位图缩放到视图的尺寸来标准化坐标并提取裁剪区域。这可能会导致裁剪后的图像像素少于将裁剪矩形缩放到位图尺寸的情况,因为视图很可能显示的是相机拍摄的照片。根据应用程序的目的,以更高的分辨率进行裁剪可能更可取。我将其留给读者作为练习。

解决方案的关键方面需要考虑如下:

  1. 裁剪矩形是相对于图像视图,而不是图像。
  2. 视图的尺寸与其渲染的图像不同。
  3. 图像的方向可能与位图的方向不同。

支持这些因素有其他方法。本解决方案的解决方式如下:

  1. 它通过将位图调整为视图的尺寸来标准化尺寸。
  2. 它在缩放位图、应用裁剪矩形和返回 `UIImage` 时考虑方向。

`MMSCropImageView` 类应该是支持您应用程序裁剪需求的良好起点。如果您使用并改进它,请将这些更改提交到 GitHub 上的项目:Swift 项目请访问 https://github.com/miller-ms/MMSCropView,Objective-C 项目请访问 https://github.com/miller-ms/MMSCropImageView

祝您的项目顺利!欢迎您提问和评论。

历史

  • 2016 年 6 月 25 日 - 上传了该解决方案的 Swift 版本,并指向了 GitHub 上的 Swift 版本项目。
  • 2016 年 2 月 6 日 - 修正了文章中显示方向的图片;澄清了一些文字;添加了更新的项目文件;新项目文件修正了创建位图上下文中的一个错误。
  • 2016 年 1 月 17 日 - 更新了变量高亮显示并更新了摘要
  • 2016 年 1 月 13 日 - 修正了类名和 GitHub 链接
  • 2016 年 1 月 12 日 - 首次发布
用于裁剪图像的视图类 - CodeProject - 代码之家
© . All rights reserved.