iOS端如何实现对图片和视频的 涂鸦,贴图,马赛克,高斯笔涂鸦等技术详解

iOS端如何实现对图片和视频的 涂鸦,贴图,马赛克,高斯笔涂鸦等技术详解

一: 图片处理部分:

1, 图片的涂鸦:

项目演示:


image.png
image.png

实现的主要思路是利用贝塞尔绘制原理:
创建一个 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, 特图和文字的实现

贴图和文字的实现原理其实是一样的,这里我们只讲如何实现贴图:


image.png

主要的大体思想就是两张图绘制到一起, 表情图片有很多的操作比如 画边框 移动 旋转 删除 隐藏划线框等等这样的 这个不在这里详细介绍,给位客观可以看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

首先看图层


image.png

首先我们需要创建一个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里面


image.png

首先我们先看看视频具体的操作

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了,这个处理的思路一般处理的是时间规定的,比如从第几秒裁剪到第几秒

视频的涂鸦贴图和加水印
image.png

这部分操作大致的思路是 将一张处理好的图片等比例 添加到视频的
图片的处理和上面图片的处理一样的
核心代码


 videoComposition.animationTool = [AVVideoCompositionCoreAnimationTool videoCompositionCoreAnimationToolWithPostProcessingAsVideoLayer:videoLayer inLayer:parentLayer];

第一个参数
重要的一步是 首先创建一个承载的视图层: CALayer *parentLayer
我们所有的操作比如加图加文字 加边框等等都是在这个 calayer上完成,然后将我们的这个parentLayer添加到 创建的 CALayer *videoLayer上

到此整片文档的技术要点都已经分析完毕,思路也已经理清楚,各位大神可以看看SDK 点赞一波了
https://github.com/wangjinshan/IJSPhotoSDK

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,311评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,339评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,671评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,252评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,253评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,031评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,340评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,973评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,466评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,937评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,039评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,701评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,254评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,259评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,497评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,786评论 2 345