前段时间公司的项目需要做自己的自定义相机和照片水印。抽空记一下笔记。文末有Demo
实现了相机的自定义 和水印、滤镜相关功能。image处理方式都是用类别的方式。
后面我会把详细的demo放到GitHub上,不好的地方欢迎指正出来一起交流
•闪光灯的自定义开关
•切换前后摄像头的开关
•相机的缩放
•相机的点击聚焦
•使用陀螺仪矫正相片的横竖屏拍照
•拍照照片的截取处理
•添加水印制作和滤镜功能
•Image的剪裁、缩放、旋转等处理方法
•水印的交互处理 (拖动、删除、放大、虚框隐藏预览)//LHStickerView
.......
一、自定义相机的准备
//首先需要引入AVFoundation 头文件 这个框架是音视频的框架 相机功能的API也是
#import
//这里拍照时判断手机的方向需要打开陀螺仪 需要这个框架
#import
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获取数据对象(摄像头获取的数据)
- (AVCaptureDeviceInput *)captureDeviceInput
{
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可以有多个输入输出
- (AVCaptureSession *)captureSession
{
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上就可以显示出来了
- (AVCaptureVideoPreviewLayer *)capturePreviewLayer
{
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);
}
三、图片处理(剪裁旋转、添加水印、系统提供的滤镜、添加文字等)
添加水印核心方法
这个方法我在项目中并没有使用 使用的是一种更粗暴的方式 图片和水印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;
}
添加滤镜
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支持横竖屏(如下图),限制太多
•我这里使用陀螺仪的监测来处理翻转方向
- (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哦 关注我哈~