iOS端如何实现对图片和视频的 涂鸦,贴图,马赛克,高斯笔涂鸦等技术详解
一: 图片处理部分:
1, 图片的涂鸦:
项目演示:
实现的主要思路是利用贝塞尔绘制原理:
创建一个 IJSIPanDrawingView 所有的操作都将在这个UIView上面完成
创建model IJSIPath 用于绘制
首先在初始化方法中创建一个手势: 在手势的方法中实现下面的代码
核心代码:
创建 IJSIPath 类 提供 属性和 添加初始化 添加点的方法 绘制方法等
初始化方法
先看 IJSIPath 这个类实现 主要的实现思路就是贝塞尔划线的思想
+ (instancetype)pathToPoint:(CGPoint)beginPoint pathWidth:(CGFloat)pathWidth
{
UIBezierPath *bezierPath = [UIBezierPath bezierPath]; // 线的路径
// 设置相关的属性 并设置起始点
[bezierPath moveToPoint:beginPoint];
// 创建自己对象 将贝塞尔数据映射到 IJSIPath 这个类中
IJSIPath *path = [[IJSIPath alloc] init];
path.bezierPath = bezierPath;
return path;
}
//曲线 这个方法主要是在移动的过程中将移动店和贝塞尔连接成线
- (void)pathLineToPoint:(CGPoint)movePoint;
{
//判断绘图类型
[self.bezierPath addLineToPoint:movePoint];
}
// 这个方法就是将上面的点连线并绘制成路径
- (void)drawPath
{
[self.pathColor set];
[self.bezierPath stroke]; // 根据坐标点连线
}
下面我们来看如何在 IJSIPanDrawingView 这个UIView中调用这个方法, 首先创建一个手势 并在手势的方法中实现;
CGPoint currentDraggingPosition = [sender locationInView:self.drawingView]; //获取到的是手指点击屏幕实时的坐标点
if (sender.state == UIGestureRecognizerStateBegan) //一个手势已经开始但尚未改变或者完成时
{
// 初始化一个UIBezierPath对象, 把起始点存储到UIBezierPath对象中, 用来存储所有的轨迹点
IJSIPath *path = [IJSIPath pathToPoint:currentDraggingPosition pathWidth:self.panWidth != 0 ? self.panWidth : MAX(1, 4)];
path.pathColor = self.panColor != nil ? self.panColor : [UIColor redColor];
[self.allLineArr addObject:path]; //添加路线
}
if (sender.state == UIGestureRecognizerStateChanged) //手势状态改变
{
// 获得数组中的最后一个UIBezierPath对象(因为我们每次都把UIBezierPath存入到数组最后一个,因此获取时也取最后一个)
IJSIPath *path = [self.allLineArr lastObject];
[path pathLineToPoint:currentDraggingPosition]; //添加点
[self drawLine]; // 划线
}
由于是在UIView中我们不采取 在 -(void)draw 方法中绘制 所以我们需要创建上下文,并将资源绘制成一张图片返回给外面,具体的代码看下面的实现
- (void)drawLine
{
CGSize size = self.drawingView.frame.size;
UIGraphicsBeginImageContextWithOptions(size, NO, 0.0); //创建一个基于位图的上下文/NO 设置透明
CGContextRef context = UIGraphicsGetCurrentContext(); // 获取当前上下文
CGContextSetAllowsAntialiasing(context, true); //去掉锯齿
CGContextSetShouldAntialias(context, true);
for (IJSIPath *path in self.allLineArr)
{
[path drawPath];
}
self.drawingView.image = UIGraphicsGetImageFromCurrentImageContext(); //生成一个image对象,并将最终的结果反馈给外界
UIGraphicsEndImageContext();
}
至此绘制的大招已经完成,如果你想开接口给外面你可以直接绘制 self.drawingView.image 就可以如:
- (UIImage *)_buildImage
{ UIGraphicsBeginImageContextWithOptions(self.originalImageSize, NO, self.backImage.scale);
[self.backImage drawAtPoint:CGPointZero];
[self.drawingView.image drawInRect:CGRectMake(0, 0, self.originalImageSize.width, self.originalImageSize.height)];
UIImage *tmp = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return tmp;
}
2, 特图和文字的实现
贴图和文字的实现原理其实是一样的,这里我们只讲如何实现贴图:
主要的大体思想就是两张图绘制到一起, 表情图片有很多的操作比如 画边框 移动 旋转 删除 隐藏划线框等等这样的 这个不在这里详细介绍,给位客观可以看SDK 的具体实现
思路分析:
首先我们会把 贴图或者文字封装到 IJSIImputTextExportView(文字) IJSIMapViewExportView(贴图),其实这个可以理解成就是一个UIView 但是这个UIView 里面有其他不同的属性
因为我们的这个项目是直接对外显示,所有我们选择在controller 的view上操作, 我们再controller上创建一个 UIImageView 名字叫drawingView, 然后把上面的UIView 全部添加到这个UIImageView上,下面就是核心的代码,这个代码决定这个绘制的结果 代码如下:
- (void)_completeCallback:(void (^)(UIImage *image))completeCallback
{
UIGraphicsBeginImageContextWithOptions(CGSizeMake(self.backImageView.image.size.width, self.backImageView.image.size.height),
NO,
self.backImageView.image.scale);
[self.backImageView.image drawAtPoint:CGPointZero];
[_drawingView.image drawInRect:CGRectMake(0, -drawY, self.backImageView.image.size.width / WS, self.backImageView.image.size.height / HS)];
for (UIView *subView in _drawingView.subviews)
{
UIView *exportView = subView;
UIImage *textImg = [self.class _screenshot:exportView orientation:UIDeviceOrientationPortrait usePresentationLayer:YES];
[textImg drawInRect:CGRectMake(exportView.js_left / selfRw, (exportView.js_top / selfRh) - drawY, sw, sh)];
}
UIImage *tmp = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
dispatch_async(dispatch_get_main_queue(), ^{
UIImage *image = [UIImage imageWithCGImage:tmp.CGImage scale:self.backImageView.image.scale orientation:UIImageOrientationUp];
if (completeCallback)
{
completeCallback(image);
}
});
}
核心的思路就是选择用的 UIKit 提供的绘制方法,一个图层一个图层的去绘制
[textImg drawInRect:CGRectMake( , , , )];
绘图部分基本完成,下面我们来讲讲 马赛格绘制和高斯模糊画笔设置
3, 马赛克绘制
我们先说说实现思路:
首先我们需要获取一张已经制作好的马赛克图, 这里我们不研究马赛克的算法,各位客官想研究马赛克的算法 请看IJSExtension UIImage的分类
下面我们假设已经获取好了UIimage
首先看图层
首先我们需要创建一个UIImage 显示完整的图片在,主要是在没有进行马赛克蒙版的是需要给用户已经显示的错觉,
下面是内容的核心:
创建一个 IJSImageMosaicView 集成自UIView
我们配置他的UI
1, 首先需要创建一个 CALayer 这一层主要用于显示上面已经获取好的马赛克图
2, 我们再创建一个 CAShapeLayer 这个主要是为了在拖动的时候绘制不规则的图形设置
3, 最关键的一点就是 将 CAShapeLayer 对象设置给 CALayer 的mask 属性 CALayer 是一个非常牛逼的属性本质还是一个 CALayer
因为mask是一个层,而且它最重要的是它的轮廓,所以mask如果是CALayer 时,常常设置它的contents属性来定义它的外观,如果mask是CAShapeLayer的时候常常会设置它的path属性,让它和UIBezierPath来结合使用定义它的外观 我们这边的思路就是根据 CAShapeLayer 来实现这个出来方案
具体代码如下:
- (void)_createdUI
{
//添加layer(imageLayer)到self上
self.imageLayer = [CALayer layer];
self.imageLayer.frame = self.bounds;
[self.layer addSublayer:self.imageLayer];
self.shapeLayer = [CAShapeLayer layer];
self.shapeLayer.strokeColor = [UIColor blueColor].CGColor; //不可设置clean, 主要是设置opacity和这个属性相关
self.shapeLayer.fillColor = nil; // 此处必需置空,不能设置
[self.layer addSublayer:self.shapeLayer];
self.imageLayer.mask = self.shapeLayer; // 子视图完全遮盖马赛克视图
}
接下来就是 通过 CAShapeLayer 的path 属性进行不规则额路径绘制等等
核心的代码只有一句话:
CGMutablePathRef path = (__bridge CGMutablePathRef)([array objectAtIndex:i]);
self.shapeLayer.path = path;
这中间会牵扯到 路径的存储等等逻辑,过程不复杂这里不仔细讲解
下面我们来谈谈 CAShapeLayer 的path
我们知道 绘制图像一般有下面几种过程
1,调用CGContextBeginPath
2,调用CGContextMoveToPoint来设置路径的起点
3,调用CGPathAddLineToPoint, CGPathAddArc, CGPathAddRect等函数添加路径
4,最后,我们需要调用fill或stroke来绘制出这些路径或图形
当我们绘制路径时,Path的信息就会被Graphics context重置。
如果我们想要保存path信息,并多次使用它,我们就可以用到CGPathCreatMutable申请路径,本文技术核心就是这种方案
在开始touch的时候:
CGPathMoveToPoint(self.path, NULL, point.x, point.y);
在touch 过程中
CGPathAddLineToPoint(self.path, NULL, point.x, point.y);
然后
CGMutablePathRef path = (__bridge CGMutablePathRef)([array objectAtIndex:i]);
self.shapeLayer.path = path;
到此整个马赛克的逻辑已经处理完成
高斯画笔的实现
下面我们再来讲讲高斯画笔的实现
高斯画笔的实现和之前涂鸦的思路基本一样 都是利用链接点的方式实现,我这里换了 更底层的方式来处理:
首先需要获取一个高斯图,这个高斯的获取方法这里不仔细讲解
首先我们将点的集合放到一个数组,组成一个路径集合,我们再讲路径的集合放到一个数组中,目的是为了有撤销的操作等等
// 创建上下文
CGContextSetStrokeColorWithColor(context, [UIColor colorWithPatternImage:self.gaussanViewGaussanImage].CGColor); // 设置高斯的颜色,就是从外界获取的颜色
// 从对应的路径数组中取出对应的点 再取出对应的,
CGContextMoveToPoint(context, p.x, p.y);
CGContextAddLineToPoint(context, p.x, p.y);
CGContextDrawPath(context, kCGPathStroke);
// 将绘制的结果存储在内存中
self.nowImage = UIGraphicsGetImageFromCurrentImageContext();
// 结束绘制
UIGraphicsEndImageContext();
[self setNeedsDisplay]; // 将内容绘制到 self.view上
最后在
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
[self.nowImage drawInRect:rect];// 直接将图片绘制到当前的view上
}
到此图片处理部分基本结束
下面我们再来讲讲 怎么处理视频
视频处理
这部分我们先来说说裁剪,这部分的内容封装到了 IJSEditSDK 这个sdk里面
首先我们先看看视频具体的操作
1 创建AVMutableComposition对象来添加视频音频资源的
AVMutableComposition *mixComposition = [[AVMutableComposition alloc] init];
2 设置采集区域
NSRange videoRange = NSMakeRange(startTime, (endTime - startTime)); // 开始位置, 裁剪的长度
CMTime startT = CMTimeMakeWithSeconds(videoRange.location, videoAsset.duration.timescale);
CMTime videoDuration = CMTimeMakeWithSeconds(videoRange.length, videoAsset.duration.timescale); //截取长度videoDuration
CMTimeRange timeRange = CMTimeRangeMake(startT, videoDuration);
3, 我们知道视频有 视频轨道和音频轨道 下面的操作就是将这连个轨道合并
// 3 - 视频通道 工程文件中的轨道,有音频轨、视频轨等,里面可以插入各种对应的素材
AVMutableCompositionTrack *videoTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeVideo
preferredTrackID:kCMPersistentTrackID_Invalid];
/*TimeRange截取的范围长度 ofTrack来源 atTime插放在视频的时间位置*/
[videoTrack insertTimeRange:timeRange
ofTrack:([videoAsset tracksWithMediaType:AVMediaTypeVideo].count > 0) ? [videoAsset tracksWithMediaType:AVMediaTypeVideo].firstObject : nil
atTime:kCMTimeZero
error:&error];
//3.2 - 添加原有音频
//视频声音采集(也可不执行这段代码不采集视频音轨,合并后的视频文件将没有视频原来的声音)
AVMutableCompositionTrack *audioTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid];
[audioTrack insertTimeRange:timeRange
ofTrack:([videoAsset tracksWithMediaType:AVMediaTypeAudio].count > 0) ? [videoAsset tracksWithMediaType:AVMediaTypeAudio].firstObject : nil
atTime:kCMTimeZero
error:&error];
通常上面的操作返回的视频,视频的转向都有问题,所以都需要做转向处理
转向的代码给位老铁去sdk里面看吧
最后一步,导出视频, 导出视频的对象是 AVAssetExportSession
对这个对象进行属性设置处理就可以了
到此视频裁剪就ok了,这个处理的思路一般处理的是时间规定的,比如从第几秒裁剪到第几秒
视频的涂鸦贴图和加水印
这部分操作大致的思路是 将一张处理好的图片等比例 添加到视频的
图片的处理和上面图片的处理一样的
核心代码
videoComposition.animationTool = [AVVideoCompositionCoreAnimationTool videoCompositionCoreAnimationToolWithPostProcessingAsVideoLayer:videoLayer inLayer:parentLayer];
第一个参数
重要的一步是 首先创建一个承载的视图层: CALayer *parentLayer
我们所有的操作比如加图加文字 加边框等等都是在这个 calayer上完成,然后将我们的这个parentLayer添加到 创建的 CALayer *videoLayer上
到此整片文档的技术要点都已经分析完毕,思路也已经理清楚,各位大神可以看看SDK 点赞一波了
https://github.com/wangjinshan/IJSPhotoSDK