# ios 绘画之涂鸦,贴图,马赛克,高斯笔涂鸦

ios 绘画之涂鸦,贴图,马赛克,高斯笔涂鸦

https://github.com/wangjinshan/IJSPhotoSDK
项目展示

001.PNG
002.PNG
003.PNG
004.PNG
005.PNG
006.PNG
007.PNG
009.PNG
010.PNG

前言

Quartz 2D是一个二维图形绘制引擎,支持iOS环境和Mac OS X环境。我们可以使用Quartz 2D API来实现许多功能,如基本路径的绘制、透明度、描影、绘制阴影、透明层、颜色管理、反锯齿、PDF文档生成和PDF元数据访问.
当你的程序进行位图绘制时,不管使用哪种方式,都是基于 Quartz 2D 的,也就是说,CPU 部分实现的绘制是通过 Quartz 2D 实现的。尽管 Quartz 可以做其它的事情,但是我们这里还是集中于位图绘制,在缓冲区(一块内存)绘制位图会包括 RGBA 数据
简单例子:
UIKit实现

UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(16.72, 7.22)];
[path addLineToPoint:CGPointMake(3.29, 20.83)];
[path addLineToPoint:CGPointMake(0.4, 18.05)];
[path addLineToPoint:CGPointMake(18.8, -0.47)];
[path addLineToPoint:CGPointMake(37.21, 18.05)];
[path addLineToPoint:CGPointMake(34.31, 20.83)];
[path addLineToPoint:CGPointMake(20.88, 7.22)];
[path addLineToPoint:CGPointMake(20.88, 42.18)];
[path addLineToPoint:CGPointMake(16.72, 42.18)];
[path addLineToPoint:CGPointMake(16.72, 7.22)];
[path closePath];
path.lineWidth = 1;
[[UIColor redColor] setStroke];
[path stroke];

相对应的 Core Graphics 代码:

CGContextBeginPath(ctx);
CGContextMoveToPoint(ctx, 16.72, 7.22);
CGContextAddLineToPoint(ctx, 3.29, 20.83);
CGContextAddLineToPoint(ctx, 0.4, 18.05);
CGContextAddLineToPoint(ctx, 18.8, -0.47);
CGContextAddLineToPoint(ctx, 37.21, 18.05);
CGContextAddLineToPoint(ctx, 34.31, 20.83);
CGContextAddLineToPoint(ctx, 20.88, 7.22);
CGContextAddLineToPoint(ctx, 20.88, 42.18);
CGContextAddLineToPoint(ctx, 16.72, 42.18);
CGContextAddLineToPoint(ctx, 16.72, 7.22);
CGContextClosePath(ctx);
CGContextSetLineWidth(ctx, 1);
CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor);
CGContextStrokePath(ctx);

ctx是什么?
正好引出所谓的 CGContext 登场。我们传过去的ctx参数正是在那个上下文中。而这个上下文定义了我们需要绘制的地方。如果我们实现了 CALayer 的 -drawInContext: 这时已经传过来一个上下文。绘制到这个上下文中的内容将会被绘制到图层的备份区(图层的缓冲区).但是我们也可以创建我们自己的上下文,叫做基于位图的上下文,比如 CGBitmapContextCreate().这个方法返回一个我们可以传给 CGContext 方法来绘制的上下文。

注意 UIKit 版本的代码为何不传入一个上下文参数到方法中?这是因为当使用 UIKit 或者 AppKit 时,上下文是唯一的。UIkit 维护着一个上下文堆栈,UIKit 方法总是绘制到最顶层的上下文中。你可以使用 UIGraphicsGetCurrentContext() 来得到最顶层的上下文。你可以使用 UIGraphicsPushContext() 和 UIGraphicsPopContext() 在 UIKit 的堆栈中推进或取出上下文。

最为突出的是,UIKit 使用 UIGraphicsBeginImageContextWithOptions() 和 UIGraphicsEndImageContext() 方便的创建类似于 CGBitmapContextCreate() 的位图上下文。混合调用 UIKit 和 Core Graphics 非常简单

UIGraphicsBeginImageContextWithOptions(CGSizeMake(45, 45), YES, 2);
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextBeginPath(ctx);
CGContextMoveToPoint(ctx, 16.72, 7.22);
CGContextAddLineToPoint(ctx, 3.29, 20.83);
...
CGContextStrokePath(ctx);
UIGraphicsEndImageContext();

解释一下UIGraphicsBeginImageContextWithOptions函数参数的含义:
第一个参数表示所要创建的图片的尺寸;
第二个参数用来指定所生成图片的背景是否为不透明,如上我们使用YES而不是NO,则我们得到的图片背景将会是黑色,显然这不是我想要的;
第三个参数指定生成图片的缩放因子,这个缩放因子与UIImage的scale属性所指的含义是一致的。传入0则表示让图片的缩放因子根据屏幕的分辨率而变化,所以我们得到的图片不管是在单分辨率还是视网膜屏上看起来都会很好
也可以写成:

CGContextRef ctx = CGBitmapContextCreate(NULL, 90, 90, 8, 90 * 4, space, bitmapInfo);
CGContextScaleCTM(ctx, 0.5, 0.5);
UIGraphicsPushContext(ctx);
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(16.72, 7.22)];
[path addLineToPoint:CGPointMake(3.29, 20.83)];
...
[path stroke];
UIGraphicsPopContext(ctx);
CGContextRelease(ctx);

注意: 必须要有UIGraphicsEndImageContext(); CGContextRelease(ctx); 否则会造成内存泄露,程序卡死

其他相关的操作

绘制

#pragma mark 绘制矩形
-(void)_drawRectWithContext:(CGContextRef)context
{
    //添加矩形对象
    CGRect rect=CGRectMake(20, 50, 280, 50);
    CGContextAddRect(context,rect);
    //设置属性
    [[UIColor blueColor]set];
    //绘制
    CGContextDrawPath(context, kCGPathFillStroke);
}
#pragma mark 绘制矩形(利用UIKit的封装方法)
-(void)_drawRectByUIKitWithContext:(CGContextRef)context
{
    CGRect rect= CGRectMake(20, 150, 280.0, 50.0);
    CGRect rect2=CGRectMake(20, 250, 280.0, 50.0);
    //设置属性
    [[UIColor yellowColor]set];
    //绘制矩形,相当于创建对象、添加对象到上下文、绘制三个步骤
    UIRectFill(rect);//绘制矩形(只有填充)
    [[UIColor redColor]setStroke];
    UIRectFrame(rect2);//绘制矩形(只有边框)
}
#pragma mark 绘制椭圆
-(void)_drawEllipse:(CGContextRef)context
{
    //添加对象,绘制椭圆(圆形)的过程也是先创建一个矩形
    CGRect rect=CGRectMake(50, 50, 220.0, 180.0);
    CGContextAddEllipseInRect(context, rect);
    //设置属性
    [[UIColor purpleColor]set];
    //绘制
    CGContextDrawPath(context, kCGPathFillStroke);
}
#pragma mark - 绘制圆弧
-(void)_drawArc:(CGContextRef)context
{
    /*添加弧形对象
     x:中心点x坐标
     y:中心点y坐标
     radius:半径
     startAngle:起始弧度
     endAngle:终止弧度
     closewise:是否逆时针绘制,0则顺时针绘制
     */
    CGContextAddArc(context, 160, 160, 100.0, 0.0, M_PI_2, 1);
    //设置属性
    [[UIColor yellowColor]set];
    //绘制
    CGContextDrawPath(context, kCGPathStroke);   // kCGPathStroke 描边  kCGPathFillStroke填充
}
#pragma mark - 绘制文字
-(void)_drawText:(CGContextRef)context
{
    //绘制到指定的区域内容
    NSString *str=@"这是一个非常牛逼的操作啊";
    CGRect rect= CGRectMake(20, 50, 280, 300);
    UIFont *font=[UIFont systemFontOfSize:18];//设置字体
    UIColor *color=[UIColor redColor];//字体颜色
    NSMutableParagraphStyle *style=[[NSMutableParagraphStyle alloc]init];//段落样式
    NSTextAlignment align=NSTextAlignmentLeft;//对齐方式
    style.alignment=align;
    NSDictionary<NSAttributedStringKey, id> *attributeDic = @{NSFontAttributeName:font,NSForegroundColorAttributeName:color,NSParagraphStyleAttributeName:style};
    [str drawInRect:rect withAttributes:attributeDic];
}
#pragma mark - 绘制图片
-(void)_drawImage:(CGContextRef)context
{
    UIImage *image=[UIImage imageNamed:@"8.png"];
    //从某一点开始绘制
    [image drawAtPoint:CGPointMake(10, 50)];
    //绘制到指定的矩形中,注意如果大小不合适会会进行拉伸
//        [image drawInRect:CGRectMake(10, 50, 300, 450)];
    //平铺绘制
//        [image drawAsPatternInRect:CGRectMake(0, 0, 320, 568)];
}
#pragma mark 图形上下文形变
-(void)_drawImageCTM:(CGContextRef)context
{
    //保存初始状态
    CGContextSaveGState(context);
    //形变第一步:图形上下文向右平移 X = 100
    CGContextTranslateCTM(context, 100, 20);
    //形变第二步:缩放0.8
    CGContextScaleCTM(context, 0.5, 0.5);
    //形变第三步:旋转
    CGContextRotateCTM(context, M_PI_4/4);
    UIImage *image=[UIImage imageNamed:@"8"];
    [image drawInRect:CGRectMake(0, 50, 240, 300)];
    //恢复到初始状态
    CGContextRestoreGState(context);
}
点击屏幕时,把控件的View截屏
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    把View的内容截屏生成一张新的图片.
    开启一个跟控制器view相同大小的上下文.
    UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, NO, 0);

    把View的内容绘制到上下文当中.
    注意:View是不能够直接绘制到上下文当中的.View之所以能够显示是因为它内部有一个layer(层),
    层是通过渲染的方式绘制到上下文当中的.
    获取当前的上下文.
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    把当前View的内容渲染到View上面.
    [self.view.layer renderInContext:ctx];
    从上下文当中生成一张图片
    UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
    关闭上下文.
    UIGraphicsEndImageContext();
    把生成的图片写到桌面上.
    桌面都是以流的形式传递数据,所以我们要把图片转成二进流.
    image:要转的图片
    compressionQuality:压缩质量,1代表质量最高
    NSData *data = UIImageJPEGRepresentation(newImage, 1);
    原始质量的png图片.
    NSData *data = UIImagePNGRepresentation(newImage);
    把二进流写到桌面.
    [data writeToFile:@"路径" atomically:YES];
}

本文主要以具体的实例讲解如何绘制

1, 随笔涂鸦

有两方案处理:
方案1: 直接在 - (void)drawRect:(CGRect)rect{} 中绘制需要的图形,因为里面已经有了上下文
方案2: 使用自己创建上下文的方式

大体的思路如下:
创建继承自UIView的 IJSIPanDrawingView, 添加手势并实现手势方法
创建 IJSIPath对象 主要存储 路径的基本信息
手势方法:

- (void)drawingViewDidPan:(UIPanGestureRecognizer*)sender
{
    CGPoint currentDraggingPosition = [sender locationInView:self.drawingView]; //获取到的是手指点击屏幕实时的坐标点
    if(sender.state == UIGestureRecognizerStateBegan)  //一个手势已经开始但尚未改变或者完成时
    {
        // 初始化一个UIBezierPath对象, 把起始点存储到UIBezierPath对象中, 用来存储所有的轨迹点
        IJSIPath *path = [IJSIPath pathToPoint:currentDraggingPosition pathWidth: self.editorController.panWidth != 0 ? self.editorController.panWidth:   MAX(1, 4)];

        path.pathColor = self.editorController.panColor != nil ? self.editorController.panColor : [UIColor redColor];
        path.shape.strokeColor =[UIColor greenColor].CGColor; //代表设置它的边框色
        [self.allLineArr addObject:path];   //添加路线
    }

    if(sender.state == UIGestureRecognizerStateChanged) //手势状态改变
    {
        // 获得数组中的最后一个UIBezierPath对象(因为我们每次都把UIBezierPath存入到数组最后一个,因此获取时也取最后一个)
        IJSIPath *path = [self.allLineArr lastObject];
        [path pathLineToPoint:currentDraggingPosition];//添加点
        [self drawLine];
        if (self.panDrawingViewdrawingCallBack) self.panDrawingViewdrawingCallBack(YES);
    }
}

滑动开始 创建 allLineArr 数组 IJSIPath对象把所有的路径信息保存到路径数组中
滑动进行中去除数组最后一个 IJSIPath对象把正在进行的所有的点添加IJSIPath对象里面
核心绘制方法:

- (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();
}
- (void)drawPath
{
    [self.pathColor set];   //填充颜色
    [self.bezierPath stroke];  // 根据坐标点连线
}

2, 贴图,或者文字

绘制文字和图片比较简单,我们采用 UIKit为我们封装的方法
比如: 首选选取图片添加到 self.view子视图,接下来我们要做的就是遍历所有的子视图并绘制

文字绘制:

- (void)drawRect:(CGRect)rect
{
    NSString *name = @"绘制文字绘制文字绘制文字绘制文字绘制文字";
    NSMutableDictionary *dic =[NSMutableDictionary dictionary];
    dic[NSFontAttributeName] = [UIFont systemFontOfSize:40];   // 字号
    NSShadow *shadow = [[NSShadow alloc]init];
    shadow.shadowOffset = CGSizeMake(10, 20); // 偏移量
    shadow.shadowBlurRadius = 2;  // 模糊度
    shadow.shadowColor = [UIColor blueColor];
    dic[NSShadowAttributeName] =shadow;
    // 绘制方法1:  自动换行
//    [name drawInRect:CGRectMake(0, 0, rect.size.width, rect.size.height) withAttributes:dic];
    // 绘制方法2:   绘制不会换行
    [name drawAtPoint:CGPointZero withAttributes:dic];
}
    drawAtPoint:要画到哪个位置
    withAttributes:文本的样式.
    [str drawAtPoint:CGPointZero withAttributes:nil];

富文本中文字的注释:

#import <Foundation/NSAttributedString.h>
字符属性
 字符属性可以应用于 attributed string 的文本中。
 NSString *const NSFontAttributeName;(字体)
 NSString *const NSParagraphStyleAttributeName;(段落)
 NSString *const NSForegroundColorAttributeName;(字体颜色)
 NSString *const NSBackgroundColorAttributeName;(字体背景色)
 NSString *const NSLigatureAttributeName;(连字符)
 NSString *const NSKernAttributeName;(字间距)
 NSString *const NSStrikethroughStyleAttributeName;(删除线)
 NSString *const NSUnderlineStyleAttributeName;(下划线)
 NSString *const NSStrokeColorAttributeName;(边线颜色)
 NSString *const NSStrokeWidthAttributeName;(边线宽度)
 NSString *const NSShadowAttributeName;(阴影)(横竖排版)
 NSString *const NSVerticalGlyphFormAttributeName;
 常量
 1> NSFontAttributeName(字体)
 该属性所对应的值是一个 UIFont 对象。该属性用于改变一段文本的字体。如果不指定该属性,则默认为12-point Helvetica(Neue)。
2> NSParagraphStyleAttributeName(段落)
 该属性所对应的值是一个 NSParagraphStyle 对象。该属性在一段文本上应用多个属性。如果不指定该属性,则默认为 NSParagraphStyle 的defaultParagraphStyle 方法返回的默认段落属性。
 3> NSForegroundColorAttributeName(字体颜色)
 该属性所对应的值是一个 UIColor 对象。该属性用于指定一段文本的字体颜色。如果不指定该属性,则默认为黑色。
 4> NSBackgroundColorAttributeName(字体背景色)
 该属性所对应的值是一个 UIColor 对象。该属性用于指定一段文本的背景颜色。如果不指定该属性,则默认无背景色。
 5> NSLigatureAttributeName(连字符)
 该属性所对应的值是一个 NSNumber 对象(整数)。连体字符是指某些连在一起的字符,它们采用单个的图元符号。0 表示没有连体字符。1 表示使用默认的连体字符。2表示使用所有连体符号。默认值为 1(注意,iOS 不支持值为 2)。
 6> NSKernAttributeName(字间距)
 该属性所对应的值是一个 NSNumber 对象(整数)。字母紧排指定了用于调整字距的像素点数。字母紧排的效果依赖于字体。值为 0 表示不使用字母紧排。默认值为0。
 7> NSStrikethroughStyleAttributeName(删除线)
该属性所对应的值是一个 NSNumber 对象(整数)。该值指定是否在文字上加上删除线,该值参考“Underline Style Attributes”。默认值是NSUnderlineStyleNone。
 8> NSUnderlineStyleAttributeName(下划线)
 该属性所对应的值是一个 NSNumber 对象(整数)。该值指定是否在文字上加上下划线,该值参考“Underline Style Attributes”。默认值是NSUnderlineStyleNone。
 9> NSStrokeColorAttributeName(边线颜色)
 该属性所对应的值是一个 UIColor 对象。如果该属性不指定(默认),则等同于 NSForegroundColorAttributeName。否则,指定为删除线或下划线颜色。更多细节见“Drawing attributedstrings that are both filled and stroked”。
 10> NSStrokeWidthAttributeName(边线宽度)
 该属性所对应的值是一个 NSNumber 对象(小数)。该值改变描边宽度(相对于字体size 的百分比)。默认为 0,即不改变。正数只改变描边宽度。负数同时改变文字的描边和填充宽度。例如,对于常见的空心字,这个值通常为3.0。
11> NSShadowAttributeName(阴影)
  该属性所对应的值是一个 NSShadow 对象。默认为 nil。
 12> NSVerticalGlyphFormAttributeName(横竖排版)
 该属性所对应的值是一个 NSNumber 对象(整数)。0 表示横排文本。1 表示竖排文本。在 iOS 中,总是使用横排文本,0 以外的值都未定义。

2,图片绘制

图片绘制和绘制文字一样

- (void)drawRect:(CGRect)rect
{
    UIImage *image =[UIImage imageNamed:@"001.png"];
    UIRectClip(CGRectMake(10, 10, 50, 50));    // 裁剪,超过的区域将裁剪
    [image drawAtPoint:CGPointZero];    //绘制出来的图图片跟图片的实际尺寸一样大
    [image drawInRect:rect];     // 拉伸图片,拉伸到指定的大小使用这个方法绘制出来的图片尺寸会和传入的rect区域一样大.
    [image drawAsPatternInRect:rect];   // 平铺
}

3, 高斯笔绘制

大体的思路就是: 获取一张高斯图,用户获取高斯图的颜色作为涂鸦笔的颜色
首先获取一张高斯图

// 根据全图获取一张高斯模糊图
- (UIImage *)getImageFilterForGaussianBlur:(int)blurNumber
{
    CGFloat blur = blurNumber * self.size.width / [UIScreen mainScreen].bounds.size.width;
    CIContext *context = [CIContext contextWithOptions:nil];
    CIImage *inputImage = [CIImage imageWithCGImage:self.CGImage];
    CIFilter *filter = [CIFilter filterWithName:@"CIGaussianBlur"
                                  keysAndValues:kCIInputImageKey, inputImage,
                        @"inputRadius", @(blur),
                        nil];
    CIImage *outputImage = filter.outputImage;
    return [UIImage imageWithCGImage:[context createCGImage:outputImage fromRect:CGRectMake(0, 0, self.size.width, self.size.height)]];
}

绘制过程和随笔涂鸦相同区别只要在下面一句

CGContextSetStrokeColorWithColor(context, [UIColor colorWithPatternImage:self.gaussanViewGaussanImage].CGColor);

此处采用底层的实现方法

- (void)drawSmearView
{
    UIGraphicsBeginImageContext(self.originImage.size); // 开启上下文
    CGContextRef context = UIGraphicsGetCurrentContext(); // 获取当前的上下
    CGContextSetLineCap(context, kCGLineCapRound); // 设置线尾的样式
    [self.originImage drawInRect:CGRectMake(0, 0,self.originImage.size.width,self.originImage.size.height)]; // 绘制原图用于地图显示
    
    CGContextSetStrokeColorWithColor(context, [UIColor colorWithPatternImage:self.gaussanViewGaussanImage].CGColor); //获取高斯图的颜色
    
    CGContextSetLineWidth(context, 10 * self.originImage.size.width / self.bounds.size.width); //线宽
    for (int i = 0 ; i < self.allLineArr.count ; i ++ ) {
        NSMutableArray *array = [self.allLineArr objectAtIndex:i];
        
        for (int i = 0 ; i < array.count ; i ++ ) {
            NSValue *value = [array objectAtIndex:i];
            CGPoint p = [value CGPointValue];
            p.x = p.x * self.originImage.size.width / self.bounds.size.width;
            p.y = p.y * self.originImage.size.height / self.bounds.size.height;
            if (i == 0) {
                CGContextMoveToPoint(context, p.x, p.y); // 设置起点
                CGContextAddLineToPoint(context, p.x, p.y); //添加移动的点
            }else{
                CGContextAddLineToPoint(context, p.x, p.y);  
            }
        }
    }
    CGContextDrawPath(context, kCGPathStroke);  //将路径绘制到上下问
    
    // 将绘制的结果存储在内存中
    self.nowImage = UIGraphicsGetImageFromCurrentImageContext();
    
    // 结束绘制
    UIGraphicsEndImageContext();
    [self setNeedsDisplay]; //重绘制
}

3, 马赛克绘制

思路: 刮刮卡
单独的view,没有底图的情况下需要创建一个 UIImageView 用于显示原图
单独的view UI结构如下:

  1. self.view 子视图是 UIImageView 用于显示没有马赛克的部分
  2. 创建 CALayer 添加到 self.layer,主要用于显示马赛克图, 在这一层的 contents 中添加马赛克的图片
  3. 创建 CAShapeLayer 主要用与在手指滑动时候显示马赛克路径, CAShapeLayer是一个通过矢量图形来绘制的图层子类,可以实现不规则路径的视图显示, 这里创建的 CAShapeLayer 只有大小没有具体内容,所以 CALayer 上的马赛克视图不会显示

非常重要的属性:

self.imageLayer.mask = self.shapeLayer;  // 子视图完全遮盖马赛克视图
mask将 self.shapeLayer; 设置成  self.imageLayer.mask 相当于 给 self.imageLayer 添加了一层面罩,罩住的部分是显示的,  没有罩住的部分不显示

代码如下:
创建 CALayer CAShapeLayer

设置self.imageLayer.mask = self.shapeLayer;  // 子视图完全遮盖马赛克视图

移动中中间的过程和之前的路径移动一样 唯一的区别在于

#pragma mark 绘制
- (void)drawSmearView
{
    for (int i = 0 ; i < self.allLineArr.count ; i ++ )
    {
        NSMutableArray *array = [self.allLineArr objectAtIndex:i];
        for (int i = 0 ; i < array.count ; i ++ )
        {
            CGMutablePathRef path = (__bridge CGMutablePathRef)([array objectAtIndex:i]);
            self.shapeLayer.path = path; // 将移动中 CGMutablePathRef 路径设置给  CAShapeLayer 可以产生一个不规则的路径,并且是透明空的
        }
    }
}

到此大功告成

关于马赛克原理的介绍:

其实给图片打码并不是在原有的图片上添加一层“蒙版”,而是使用各个平台提供的API去操作像素点,认为的干扰了像素点,就实现了马赛克的效果,以下面两幅图为例子,介绍一下如何的去“干扰像素”。

    在图像学中,如果你想去对图片进行处理,就必须得知道一个概念什么是“位图图像”。位图图像(bitmap), 亦称为点阵图像或绘制图像,是由称作像素(图片元素)的单个点组成的。这些点可以进行不同的排列和染色以构成图样。当放大位图时,可以看见赖以构成整个图像的无数单个方块。扩大位图尺寸的效果是增大单个像素,从而使线条和形状显得参差不齐。然而,如果从稍远的位置观看它,位图图像的颜色和形状又显得是连续的

图解:
1, 假如一个图像是由6 * 9 = 72个像素组成,现将一个像素点放大到图1方块单位大小。

马赛克原理.png

2, 坐标系
现在以左下角第一个方块为原点将图像纳入坐标系中,如下图所示。马赛克效果实际上是在原始图片的起始位置(0,8)到(2,6)其中包含了9个像素(马赛克矩形3 * 3)。

马赛克.png

马赛克矩形:
这里所说的马赛克矩形,指的是N个三位像素所组成的矩形(这里使用3*3),使每一个矩形的ARGB都和第一个矩形的ARGB相同,就达到了破坏原有图像的效果

代码

- (UIImage *)getMosaicImageFromOrginImageBlockLevel:(NSUInteger)level
{
    // self == OrginImage
    //1,获取BitmapData
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();  // 创建颜色空间
    CGImageRef imgRef = self.CGImage;   // 图片转换
    CGFloat width = CGImageGetWidth(imgRef);  //图片宽
    CGFloat height = CGImageGetHeight(imgRef); //高
   
    // 2, 创建图片上下文(解析图片信息,绘制图片 开辟内存空间,这块空间用于处理马赛克图片
    CGContextRef context = CGBitmapContextCreate (nil,  //数据源
                                                  width,
                                                  height,
                                                  kBitsPerComponent,  //每个颜色值8bit,图像学中,像素点:ARGB组成 每一个表示一个分量(例如A,R,G,B),
                                                  width*kPixelChannelCount, //每一行的像素点占用的字节数,每个像素点的ARGB四个通道各占8个bit
                                                  colorSpace,  // 颜色空间
                                                  kCGImageAlphaPremultipliedLast); //是否需要透明度
    // 3, 根据图片上下文绘制图片
    CGContextDrawImage(context, CGRectMake(0, 0, width, height), imgRef);
    
    // 4, 获取图片的像素数组
    unsigned char *bitmapData = CGBitmapContextGetData (context);
    
    //5, 核心算法 图片打码,加入马赛克,这里把BitmapData进行马赛克转换,让一个像素点替换为和它相同的矩形区域(正方形,圆形都可以)
    unsigned char pixel[kPixelChannelCount] = {0}; // 像素点默认是4个通道,默认值是0
    NSUInteger index,preIndex;
    for (NSUInteger i = 0; i < height - 1 ; i++)
    {
        for (NSUInteger j = 0; j < width - 1; j++)
        {
            index = i * width + j;  // 获取当前像素点坐标
            if (i % level == 0)
            {
                if (j % level == 0)
                {
                    memcpy(pixel, bitmapData + kPixelChannelCount*index, kPixelChannelCount); //给我们的像素点赋值
                }else{
                    memcpy(bitmapData + kPixelChannelCount*index, pixel, kPixelChannelCount);
                }
            } else {
                preIndex = (i-1)*width +j;
                memcpy(bitmapData + kPixelChannelCount*index, bitmapData + kPixelChannelCount*preIndex, kPixelChannelCount);
            }
        }
    }
    
    // 6, 获取图片数据集合
    NSInteger dataLength = width*height* kPixelChannelCount;
    CGDataProviderRef provider = CGDataProviderCreateWithData(NULL, bitmapData, dataLength, NULL);
    
    //7, 创建要输出的图像
    CGImageRef mosaicImageRef = CGImageCreate(width, height,
                                              kBitsPerComponent,  //表示每一个像素点,每一个分量的大小
                                              kBitsPerPixel,   //每一个像素点的大小
                                              width*kPixelChannelCount ,  //每一行内存大小
                                              colorSpace,      //颜色空间
                                              kCGBitmapByteOrderDefault,  //位图信息
                                              provider,      //数据源(数据集合)
                                              NULL,         //数据解码器
                                              NO,             // 是否抗锯齿
                                              kCGRenderingIntentDefault);   //渲染器
    
    // 8 创建输出马赛克图片(填充颜色)
    CGContextRef outputContext = CGBitmapContextCreate(nil,
                                                       width,
                                                       height,
                                                       kBitsPerComponent,
                                                       width*kPixelChannelCount,
                                                       colorSpace,
                                                       kCGImageAlphaPremultipliedLast);
    CGContextDrawImage(outputContext, CGRectMake(0.0f, 0.0f, width, height), mosaicImageRef); //  //绘制图片
    CGImageRef resultImageRef = CGBitmapContextCreateImage(outputContext);  // //创建图片
    UIImage *resultImage = nil;
    if([UIImage respondsToSelector:@selector(imageWithCGImage:scale:orientation:)])
    {
        float scale = [[UIScreen mainScreen] scale];
        resultImage = [UIImage imageWithCGImage:resultImageRef scale:scale orientation:UIImageOrientationUp];
    } else {
        resultImage = [UIImage imageWithCGImage:resultImageRef];
    }
    //释放
    if(resultImageRef)
    {
        CFRelease(resultImageRef);
    }
    if(mosaicImageRef)
    {
        CFRelease(mosaicImageRef);
    }
    if(colorSpace)
    {
        CGColorSpaceRelease(colorSpace);
    }
    if(provider)
    {
        CGDataProviderRelease(provider);
    }
    if(context)
    {
        CGContextRelease(context);
    }
    if(outputContext)
    {
        CGContextRelease(outputContext);
    }
    return resultImage;
}

ios图像知识

1,什么是图形图像

一张图像就是像素点的集合,每一个像素都是一个单独,明了的颜色。图像一般情况下都存储成数组,你可以把他们相像成2维数组。

这一张是缩放版本的幽灵,被放大后:

图像中这些小的“方块”就是像素,每一像素只表示一种颜色。当成百上千万的像素集体到一起后,就构成了图形图像。

如何用字节来表示颜色

表示图形的方式有许多种。在本教程中使用的是最简单的:32位RGBA模式。

如同它的名字一样,32位 RGBA 模式会将一个颜色值存储在32位,或者4个字节中。每一个字节存储一个部分或者一个颜色通道。这4个部分分别是:

~ R代表红色

~ G代表绿色

~ B代表蓝色

~ A代表透明度

正如你所知道的,红,绿和蓝是所有颜色的基本颜色集。你几乎可以使用他们创建搭配出任何想要的颜色。

由于使用8位表示每一种颜色值,那么使用32位RGBA模式实际上可以创建出不透明的颜色的总数是256256256种,已经接近17亿种。惊叹,那是好多好多好多的颜色!

alpha通道与其它的不同。你可以把它当成透明的东西,就像UIView的alpah属性。

透明颜色意味着没有任何的颜色,除非在它的后面有另外一种颜色;它的主要功能就是要告诉图像处理这个像素的透明度是多少,于是,就会有多少颜色值穿透过它而显示出来。

你将会通过本节后面的内容更新深入的了解。

总结一下,一个图形就是像素的集体,并且每一个像素只能表示一种颜色。本节,你已经了解了32位RGBA模式。

提示:你有没有想过,位图的结构组成?一张位图就是一张2D的地图,每一块就是一个像素!像素就是地图的每一块。

现在你已经了解了用字节表示颜色的基础了。不过在你开始着手写代码前,还有三个以上的概念需要你了解。

2,颜色空间

使用RGB模式表示颜色是颜色空间的一个例子。它只是众多存储颜色方法中的一种。另外一种颜色空间是灰阶空间。像它的名字一样,所有的图形都只有黑和白,只需要保存一个值来表示这种颜色。

下面这种使用RGB模式表示的颜色,人类的肉眼是很难识别的。

Red: 0 Green:104 Blue:55

你认为RGB值为[0,104,55]会产生一种什么颜色?

认真的思考一下,你也许会说是一种蓝绿色或者绿色,但那是错的。原来,你所看到的是深绿色。

另外两种比较常见的颜色空间是HSV和YUV。

HSV,使用色调,饱和度和亮度来直观的存储颜色值。你可以把这三个部分这样来看:

·色调就是颜色
·饱和度就是这个颜色有多么的饱满
·值就是颜色的亮度有多亮

在这种颜色空间中,如果你发现自己并不知道HSV的值,那么通过它的三个值,可以很容易的相像出大概是什么颜色。

RGB和HSV颜色空间的区别是很容易理解的,请看下面的图像:

YUV是另外一种常见的颜色空间,电视机使用的就是这种方式。

最开始的时候,电视机只有灰阶空间一种颜色通道。后来,当彩色电影出现后,就有了2种通道。当然,如果你想在本教程中使用YUV,那么你需要去研究更多关于YUV和其它颜色空间的相关知识。

NOTE:同样的颜色空间,你也可以使用不同的方法表示颜色。比如16位RGB模式,可以使用5个字节存储R,6个字节存储G,5个字节存储B。

为什么用6个字节存储绿色,5个字节存储蓝色?这是一个有意思的问题,答案就是因为眼球。人类的眼球对绿色比较敏感,所以人类的眼球更空间分辨出绿色的颜色值变化。

3,坐标系统

既然一个图形是由像素构成的平面地图,那么图像的原点需要说明一下。通常原点在图像的左上角,Y轴向下;或者原点在图像的左下,Y轴向上。

没有固定的坐标系统,苹果在不同的地方可能会使用不同的坐标系。

目前,UIImage和UIView使用的是左上原点坐标,Core Image和Core Graphics使用的是左下原点坐标。这个概念很重要,当你遇到图像绘制倒立问题的时候你就知道了。

4,图形压缩

这是在你开始编写代码前的最后一个需要了解的概念了!原图的每一个像素都被存储在各自的内存中。

如果你使用一张8像素的图形做运算,它将会消耗810^6像素4比特/像素=32兆字节内存。关注一下数据!

这就是为什么会出现jpeg,png和其它图形格式的原因。这些都是图形压缩格式。

当GPU在绘制图像的时候,会使用大量内存把图像的原始尺寸进行解压缩。如果你的程序占用了过多的内存,那么操作系统会将进程杀死(程序崩溃)。所以请确定你的程序使用较大的图像进行过测试。

关注一下像素

图片加载到你的手机上会看到如下的图像:

在控制台,你会看到如下的输出:

当前的程序可以加载这张幽灵的图像,并得到图像的所有像素值,打印出每个像素的亮度值到日志中。

亮度值是神马?它就是红色,绿色和蓝色通过的平均值。

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

推荐阅读更多精彩内容