傲视苍穹iOS《Objective-C》VIP专题iOS大咖说iOS精品文章

iOS自定义水印相机

2017-07-28  本文已影响348人  iii余光

前段时间公司的项目需要做自己的自定义相机和照片水印。抽空记一下笔记。

实现了相机的自定义 和水印、滤镜相关功能。image处理方式都是用类别的方式。
后面我会把详细的demo放到GitHub上,不好的地方欢迎指正出来一起交流

•闪光灯的自定义开关
•切换前后摄像头的开关
•相机的缩放
•相机的点击聚焦
•使用陀螺仪矫正相片的横竖屏拍照
•拍照照片的截取处理
•添加水印制作和滤镜功能
•Image的剪裁、缩放、旋转等处理方法
•水印的交互处理 (拖动、删除、放大、虚框隐藏预览)//LHStickerView
.......

IMG_0009.PNG

一、自定义相机的准备

//首先需要引入AVFoundation 头文件  这个框架是音视频的框架 相机功能的API也是
#import <AVFoundation/AVFoundation.h>
//这里拍照时判断手机的方向需要打开陀螺仪 需要这个框架
#import <CoreMotion/CoreMotion.h>

AVCaptureDevice
AVCaptureDeviceInput
AVCaptureStillImageOutput(AVCaptureOutput的子类 下文会有介绍)
AVCaptureSession (相机设备获取数据的管理者)
AVCaptureVideoPreviewLayer

AVCaptureDevice
捕获设备,通常是前置摄像头,后置摄像头,麦克风(音频输入)
这个类是用来管理手机设备的硬件相关的属性和配置 在这里用来获取前后摄像头 (聚焦、白平衡等、闪光灯、)手电筒也会用到哦

- (AVCaptureDevice *)captureDevice { if (_captureDevice == nil) { //使用AVMediaTypeVideo 指明self.device代表视频,默认使用后置摄像头进行初始 化 _captureDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; } return _captureDevice; }

详细了解可以看这篇文章—>关于AVCaptureDevice

AVCaptureDeviceInput

设备输入数据管理对象,可以根据AVCaptureDevice创建对应的AVCaptureDeviceInput对象,该对象将会被添加到AVCaptureSession中管理
输入数据管理对象负责从AVCaptureDevice获取数据对象(摄像头获取的数据)

{
  if (_captureDeviceInput == nil) {
  //使用设备初始化输入
_captureDeviceInput = [[AVCaptureDeviceInput alloc]initWithDevice:self.captureDevice
error:nil];
}
return _captureDeviceInput;
}

AVCaptureOutput

有input当然得有output了
这是一个抽象类,描述 AVCaptureSession 的结果。以下是三种关于静态图片捕捉的具体子类:

AVCaptureStillImageOutput 用于捕捉静态图片
AVCaptureMetadataOutput 启用检测人脸和二维码
AVCaptureVideoDataOutput 为实时预览图提供原始帧

这里当然用的是AVCaptureStillImageOutput* 这个子类来获取捕捉到的图像描述数据了*

{
if (_imageOutPut == nil) {
//生成输出对象
_imageOutPut = [[AVCaptureStillImageOutput alloc] init];
NSDictionary *myOutputSettings = [[NSDictionary alloc] initWithObjectsAndKeys:AVVideoCodecJPEG,AVVideoCodecKey,nil];
[_imageOutPut setOutputSettings:myOutputSettings];
}
return _imageOutPut;
}

AVCaptureSession

媒体(音、视频)捕获会话,负责把捕获的音视频数据输出到输出设备中。一个AVCaptureSession可以有多个输入输出

{
if (_captureSession == nil) {
//生成会话,用来结合输入输出
_captureSession = [[AVCaptureSession alloc]init];
if ([_captureSession canSetSessionPreset:AVCaptureSessionPresetPhoto]) {
//使用AVCaptureSessionPresetPhoto会自动设置为最适合的拍照配置。比如它可以允许我们使用最高的感光度 (ISO) 和曝光时间,基于相位检测的自动对焦, 以及输出全分辨率的 JPEG 格式压缩的静态图片。
_captureSession.sessionPreset = AVCaptureSessionPresetPhoto;
}
if ([_captureSession canAddInput:self.captureDeviceInput]) {
[_captureSession addInput:self.captureDeviceInput];
}
if ([_captureSession canAddOutput:self.imageOutPut]) {
[_captureSession addOutput:self.imageOutPut];
}
}
return _captureSession;
}

这个类比较重要 苹果一贯的方式用session来管理和桥接 包括新的ARKit 也是用ARSession来管理数据的

AVCaptureVideoPreviewLayer

CALayer的子类,可被用于自动显示相机产生的实时图像。它还有几个工具性质的方法,可将 layer 上的坐标转化到设备上。它看起来像输出,但其实不是。另外,它拥有session (outputs 被 session所拥有)。
写到这个类的时候就是相机该出现的时候了 创建好之后add到你的View上就可以显示出来了

{
if (_capturePreviewLayer == nil) {
_capturePreviewLayer = [[AVCaptureVideoPreviewLayer alloc]initWithSession:self.captureSession];
_capturePreviewLayer.backgroundColor = [UIColor blackColor].CGColor;
CGRect layerRect = [[self layer] bounds];
[_capturePreviewLayer setBounds:layerRect];
[_capturePreviewLayer setPosition:CGPointMake(CGRectGetMidX(layerRect),CGRectGetMidY(layerRect))];
_capturePreviewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
}
return _capturePreviewLayer;
}

二、代码功能实现

1 . AVCaptureDevice是用来控制硬件的接口。在拍照的时候我们需要一个摄像头的设备。因此我们需要遍历所有设备找到相应的摄像头。

NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
for ( AVCaptureDevice *device in devices )
if ( device.position == position ) return device;
return nil;
}

2. 设置并添加相机 开始启动

- (void)customCamera{
        
    [self.layer addSublayer:self.capturePreviewLayer];

    if ([self.captureDevice lockForConfiguration:nil]) {
        
        [self flashSwitch:self.flashState];
        //自动白平衡
        if ([self.captureDevice isWhiteBalanceModeSupported:AVCaptureWhiteBalanceModeAutoWhiteBalance]) {
            [self.captureDevice setWhiteBalanceMode:AVCaptureWhiteBalanceModeAutoWhiteBalance];
        }
        [self.captureDevice unlockForConfiguration];
    }
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        //开始启动 一般关于相机的操作最好开一个线程去操作
        [self.captureSession startRunning];
    });
   
}

**3. 给View上添加tap来实现相机的聚焦框 **

#pragma mark -- 聚焦框
- (void)focusGesture:(UITapGestureRecognizer*)gesture{
    CGPoint point = [gesture locationInView:gesture.view];
    [self focusAtPoint:point];
}
- (void)focusAtPoint:(CGPoint)point{
    
    CGSize size = self.bounds.size;
    CGPoint focusPoint = CGPointMake( point.y /size.height ,1-point.x/size.width );
    NSError *error;
    if ([self.captureDevice lockForConfiguration:&error]) {
        
        if ([self.captureDevice isFocusModeSupported:AVCaptureFocusModeAutoFocus]) {
            [self.captureDevice setFocusPointOfInterest:focusPoint];
            [self.captureDevice setFocusMode:AVCaptureFocusModeAutoFocus];
        }
        
        if ([self.captureDevice isExposureModeSupported:AVCaptureExposureModeAutoExpose ]) {
            [self.captureDevice setExposurePointOfInterest:focusPoint];
            [self.captureDevice setExposureMode:AVCaptureExposureModeAutoExpose];
        }
        
        [self.captureDevice unlockForConfiguration];
        self.focusView.center = point;
        self.focusView.hidden = NO;
        
        [UIView animateWithDuration:0.3 animations:^{
            self.focusView.transform = CGAffineTransformMakeScale(1.25, 1.25);
        }completion:^(BOOL finished) {
            [UIView animateWithDuration:0.5 animations:^{
                self.focusView.transform = CGAffineTransformIdentity;
            } completion:^(BOOL finished) {
                self.focusView.hidden = YES;
            }];
        }];
    }
    
}

4.摄像头的配置
闪光灯开关功能

#pragma mark -- 闪光灯开关
- (void)flashSwitch:(LHCaptureViewFlashSwitch)switchModel{
    AVCaptureFlashMode flashModel = (AVCaptureFlashMode)switchModel;
    if ([self.captureDevice lockForConfiguration:nil]) {
        
        if ([self.captureDevice isFlashModeSupported:flashModel]) {
            [self.captureDevice setFlashMode:flashModel];
        }
        [self.captureDevice unlockForConfiguration];
    }
}

切换前后摄像头

#pragma mark -- 改变前后摄像头
- (void)changeCamera{
    NSUInteger cameraCount = [[AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo] count];
    if (cameraCount > 1) {
        NSError *error;
        
        CATransition *animation = [CATransition animation];
        animation.duration = .5f;
        animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
        animation.type = @"oglFlip";
        
        AVCaptureDevice *newCamera = nil;
        AVCaptureDeviceInput *newInput = nil;
        AVCaptureDevicePosition position = [[self.captureDeviceInput device] position];
        if (position == AVCaptureDevicePositionFront){
            newCamera = [self cameraWithPosition:AVCaptureDevicePositionBack];
            animation.subtype = kCATransitionFromLeft;
        }
        else {
            newCamera = [self cameraWithPosition:AVCaptureDevicePositionFront];
            animation.subtype = kCATransitionFromRight;
        }
        
        newInput = [AVCaptureDeviceInput deviceInputWithDevice:newCamera error:nil];
        [self.capturePreviewLayer addAnimation:animation forKey:nil];
        if (newInput != nil) {
            
            [self.captureSession beginConfiguration];
            [self.captureSession removeInput:self.captureDeviceInput];
            if ([self.captureSession canAddInput:newInput]) {
                [self.captureSession addInput:newInput];
                self.captureDeviceInput = newInput;
                
            } else {
                [self.captureSession addInput:self.captureDeviceInput];
            }
            
            [self.captureSession commitConfiguration];
            
        } else if (error) {
            NSLog(@"toggle carema failed, error = %@", error);
        }
        
    }
}

5. 最重要的拍照 原理应该是截取输入帧的当前帧作为一张JPG 做了旋转矫正等处理

#pragma mark - 拍照 截取照片
- (void)shutterCamera
{
    AVCaptureConnection * videoConnection = [self.imageOutPut connectionWithMediaType:AVMediaTypeVideo];
    if (!videoConnection) {
        NSLog(@"take photo failed!");
        return;
    }

    [videoConnection setVideoScaleAndCropFactor:_effectiveScale];

    __weak typeof(self) weakSelf = self;
    [self.imageOutPut captureStillImageAsynchronouslyFromConnection:videoConnection
                                                  completionHandler:^(CMSampleBufferRef imageDataSampleBuffer, NSError *error) {
        if (imageDataSampleBuffer == NULL) {
            return;
        }
        NSData * imageData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageDataSampleBuffer];
        
        UIImage *originImage = [[UIImage alloc] initWithData:imageData];
        CGSize size = CGSizeMake(weakSelf.capturePreviewLayer.bounds.size.width * 2,
                                 weakSelf.capturePreviewLayer.bounds.size.height * 2);
        
        UIImage *scaledImage = [originImage resizedImageWithContentMode:UIViewContentModeScaleAspectFill
                                                                 bounds:size
                                                   interpolationQuality:kCGInterpolationHigh];
        
        CGRect cropFrame = CGRectMake((scaledImage.size.width - size.width) / 2,
                                      (scaledImage.size.height - size.height) / 2,
                                      size.width, size.height);
        UIImage *croppedImage = nil;
        if (weakSelf.captureDeviceInput.device.position == AVCaptureDevicePositionFront) {
            croppedImage = [scaledImage croppedImage:cropFrame
                                     WithOrientation:UIImageOrientationUpMirrored];
        }else
        {
            croppedImage = [scaledImage croppedImage:cropFrame];
        }
        //横屏时旋转image
        croppedImage = [croppedImage changeImageWithOrientation:_orientation];
        if (weakSelf.delegate && [weakSelf.delegate respondsToSelector:@selector(shutterCameraWithImage:)]) {
            [weakSelf.delegate shutterCameraWithImage:croppedImage];
        }
    }];
}

6. 保存到相册

#pragma - 保存至相册
+ (void)saveImageToPhotoAlbum:(UIImage*)savedImage
{
UIImageWriteToSavedPhotosAlbum(savedImage, self, nil, NULL);
}

三、图片处理(剪裁旋转、添加水印、系统提供的滤镜、添加文字等)

添加水印效果 •紫霞Cosplay
添加水印核心方法

这个方法我在项目中并没有使用 使用的是一种更粗暴的方式 图片和水印logo 在同一个View上 所以直接截取整个View作为一个Image 截取的时候修正一些分辨率的属性就行

/**
 *  同上
 *
 *  @param str     需要添加水印的文字
 *  @param strRect 文字的位置大小
 *  @param attri   富文本属性
 *  @param image   水印logo
 *  @param imgRect 图片的位置大小
 *  @param alpha   透明度
 *
 *  @return 同上
 */
- (UIImage*)imageWaterMarkWithString:(NSString*)str rect:(CGRect)strRect attribute:(NSDictionary *)attri image:(UIImage *)image imageRect:(CGRect)imgRect alpha:(CGFloat)alpha
{
    UIGraphicsBeginImageContext(self.size);
    [self drawInRect:CGRectMake(0, 0, self.size.width, self.size.height) blendMode:kCGBlendModeNormal alpha:1.0];
    if (image) {
        [image drawInRect:imgRect blendMode:kCGBlendModeNormal alpha:alpha];
    }
    
    if (str) {
        [str drawInRect:strRect withAttributes:attri];
    }
    UIImage *resultImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return resultImage;
}
-(UIImage *)waterWithWaterImage:(UIImage *)waterImage
                      waterSize:(CGSize)waterSize
                       marginXY:(CGPoint)marginXY{
    
    CGSize size = self.size;
    
    CGRect rect = (CGRect){CGPointZero,size};
    
    //新建图片图形上下文
    UIGraphicsBeginImageContextWithOptions(size, NO, 0.0f);
    
    //绘制图片
    [self drawInRect:rect];
    
    //计算水印的rect
    CGSize waterImageSize = CGSizeEqualToSize(waterSize, CGSizeZero)?waterImage.size:waterSize;
    
    //绘制水印图片
    [waterImage drawInRect:CGRectMake(marginXY.x, marginXY.y, waterImageSize.width, waterImageSize.height)];
    
    //获取图片
    UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
    
    //结束图片图形上下文
    UIGraphicsEndImageContext();
    
    return newImage;
}

添加滤镜

滤镜(黑白) •不知火舞cosplay

UIImage+Filter 这个类别负责对image进行滤镜处理
用例中列出了系统支持的滤镜类型

//------------滤镜--------------------\\

// 怀旧 --> CIPhotoEffectInstant                         单色 --> CIPhotoEffectMono
// 黑白 --> CIPhotoEffectNoir                            褪色 --> CIPhotoEffectFade
// 色调 --> CIPhotoEffectTonal                           冲印 --> CIPhotoEffectProcess
// 岁月 --> CIPhotoEffectTransfer                        铬黄 --> CIPhotoEffectChrome
// CILinearToSRGBToneCurve, CISRGBToneCurveToLinear, CIGaussianBlur, CIBoxBlur, CIDiscBlur, CISepiaTone, CIDepthOfField
/**对图片进行滤镜处理*/
+ (UIImage *)filterWithOriginalImage:(UIImage *)image filterName:(NSString *)name{
    CIContext *context = [CIContext contextWithOptions:nil];
    CIImage *inputImage = [[CIImage alloc] initWithImage:image];
    CIFilter *filter = [CIFilter filterWithName:name];
    [filter setValue:inputImage forKey:kCIInputImageKey];
    CIImage *result = [filter valueForKey:kCIOutputImageKey];
    CGImageRef cgImage = [context createCGImage:result fromRect:[result extent]];
    UIImage *resultImage = [UIImage imageWithCGImage:cgImage];
    CGImageRelease(cgImage);
    return resultImage;
}

截取当前View作为一个Image 可以保存到相册

UIImage+Resize 项目中这个category负责处理image的size (剪切、旋转、缩放、方向...)

+ (UIImage *)croppedImageFromView:(UIView *)theView
{
    CGSize orgSize = theView.bounds.size ;
    UIGraphicsBeginImageContextWithOptions(orgSize, YES, theView.layer.contentsScale * 3);
    [theView.layer renderInContext:UIGraphicsGetCurrentContext()]   ;
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext()    ;
    UIGraphicsEndImageContext() ;
    
    return image ;
}

用前置摄像头拍出来的照片发现左右不对,这时候就需要这个方法来调整一下(方向对称)

typedef NS_ENUM(NSInteger, UIImageOrientation) {
    UIImageOrientationUp,            // default orientation
    UIImageOrientationDown,          // 180 deg rotation
    UIImageOrientationLeft,          // 90 deg CCW
    UIImageOrientationRight,         // 90 deg CW
    UIImageOrientationUpMirrored,    // as above but image mirrored along other axis. horizontal flip
    UIImageOrientationDownMirrored,  // horizontal flip
    UIImageOrientationLeftMirrored,  // vertical flip
    UIImageOrientationRightMirrored, // vertical flip
};
//上面的type是Image支持的方向类型 前置摄像头 我会使用UIImageOrientationUpMirrored
- (UIImage *)croppedImage:(CGRect)bounds WithOrientation:(UIImageOrientation)orientation
{
    CGImageRef croppedCGImage = CGImageCreateWithImageInRect(self.CGImage ,bounds);
    UIImage *croppedImage = [UIImage imageWithCGImage:croppedCGImage scale:1.0f orientation:orientation];
    CGImageRelease(croppedCGImage);
    
    return croppedImage;
}

旋转图片

- (UIImage *)changeImageWithOrientation:(UIDeviceOrientation)deviceOrientation
{
    if (deviceOrientation != UIDeviceOrientationPortrait) {
        CGFloat degree = 0;
        switch (deviceOrientation) {
            case UIDeviceOrientationPortraitUpsideDown:
                degree = 180;//M_PI
                break;
            case UIDeviceOrientationLandscapeLeft:{
                if (self.imageOrientation == UIImageOrientationUpMirrored) {
                    degree = 90;
                }else
                    degree = - 90;//-M_PI_2
            }
                break;
            case UIDeviceOrientationLandscapeRight:{
                if (self.imageOrientation == UIImageOrientationUpMirrored) {
                    degree = - 90;
                }else
                    degree = 90;//M_PI_2
            }
                break;
            default:
                break;
        }
    return  [self rotatedByDegrees:degree];

    }
    return self;
}

- (UIImage *)rotatedByDegrees:(CGFloat)degrees
{
    // calculate the size of the rotated view's containing box for our drawing space
    UIView *rotatedViewBox = [[UIView alloc] initWithFrame:CGRectMake(0,0,self.size.width, self.size.height)];
    CGAffineTransform t = CGAffineTransformMakeRotation(DegreesToRadians(degrees));
    rotatedViewBox.transform = t;
    CGSize rotatedSize = rotatedViewBox.frame.size;
    
    // Create the bitmap context
    UIGraphicsBeginImageContext(rotatedSize);
    CGContextRef bitmap = UIGraphicsGetCurrentContext();
    
    // Move the origin to the middle of the image so we will rotate and scale around the center.
    CGContextTranslateCTM(bitmap, rotatedSize.width/2, rotatedSize.height/2);
    
    //   // Rotate the image context
    CGContextRotateCTM(bitmap, DegreesToRadians(degrees));
    
    // Now, draw the rotated/scaled image into the context
    CGContextScaleCTM(bitmap, 1.0, -1.0);
    CGContextDrawImage(bitmap, CGRectMake(-self.size.width / 2, -self.size.height / 2, self.size.width, self.size.height), [self CGImage]);
    
    UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return newImage;
    
    
}

开启陀螺仪获取当前设备方向

需求是拍照的时候要处理横竖屏 一般有两个方式来做
•简单的就是设备手机的旋转锁屏是打开的&你的APP支持横竖屏(如下图),限制太多
•我这里使用陀螺仪的监测来处理翻转方向

屏幕快照 2017-08-03 上午11.21.42.png
- (CMMotionManager *)cmmotionManager
{
    if (_cmmotionManager == nil) {
        _cmmotionManager = [[CMMotionManager alloc]init];
    }
    return _cmmotionManager;
}

- (void)startAccelerometerUpdates
{
    
    if([self.cmmotionManager isDeviceMotionAvailable]) {

       __weak typeof(self) weakSelf = self;
        
        [self.cmmotionManager startAccelerometerUpdatesToQueue:[NSOperationQueue currentQueue] withHandler:^(CMAccelerometerData * _Nullable accelerometerData, NSError * _Nullable error) {
            
            CGFloat xx = accelerometerData.acceleration.x;
            CGFloat yy = -accelerometerData.acceleration.y;
            CGFloat zz = accelerometerData.acceleration.z;
            CGFloat device_angle = M_PI / 2.0f - atan2(yy, xx);
            
            if (device_angle > M_PI){
                device_angle -= 2 * M_PI;
            }
            
            if ((zz < -.60f) || (zz > .60f)) {
                weakSelf.orientation = UIDeviceOrientationUnknown;
            }else{
                if ( (device_angle > -M_PI_4) && (device_angle < M_PI_4) ){
                    weakSelf.orientation = UIDeviceOrientationPortrait;
                }
                else if ((device_angle < -M_PI_4) && (device_angle > -3 * M_PI_4)){
                    weakSelf.orientation = UIDeviceOrientationLandscapeLeft;
                }
                else if ((device_angle > M_PI_4) && (device_angle < 3 * M_PI_4)){
                    weakSelf.orientation = UIDeviceOrientationLandscapeRight;
                }
                else{
                    weakSelf.orientation = UIDeviceOrientationPortraitUpsideDown;
                }
            }
            //NSLog(@"============= %f %ld",device_angle,(long)weakSelf.orientation);
        }];

    }
}
横屏效果

Demo地址

终于有时间整理了一下 功能齐全

相机被我抽象成了一个LHCaptureView 只使用相机的话很方便 只需要添加到你的View上就可以了 接口都有注释 易理解

帮到你了可以给个star哦
LHCamera

上一篇下一篇

猜你喜欢

热点阅读