# 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閱讀 228,983評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,772評論 3 422
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,947評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,201評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,960評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,350評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,406評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,549評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,104評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,914評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,089評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,647評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,340評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,753評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,007評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,834評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,106評論 2 375

推薦閱讀更多精彩內容