前段時間公司的項目需要做自己的自定義相機和照片水印。抽空記一下筆記。文末有Demo
實現了相機的自定義 和水印、濾鏡相關功能。image處理方式都是用類別的方式。
后面我會把詳細的demo放到GitHub上,不好的地方歡迎指正出來一起交流
?閃光燈的自定義開關
?切換前后攝像頭的開關
?相機的縮放
?相機的點擊聚焦
?使用陀螺儀矯正相片的橫豎屏拍照
?拍照照片的截取處理
?添加水印制作和濾鏡功能
?Image的剪裁、縮放、旋轉等處理方法
?水印的交互處理 (拖動、刪除、放大、虛框隱藏預覽)//LHStickerView
.......
一、自定義相機的準備
//首先需要引入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獲取數據對象(攝像頭獲取的數據)
- (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哦 關注我哈~