【iOS】Bitmap 位图详解与实践

在工作中经常会遇到要对UIImage做各种处理,如旋转、放大缩小、裁剪等等,更深入的则可能会对图片上的像素进行操作。最近更深入地了解了一下位图(Bitmap)的相关知识。下面文章主要分为一下几个部分:

  • 了解Bitmap
  • iOS中的Bitmap
  • 代码实践

了解Bitmap

位图Bitmap),又称栅格图(英语:Raster graphics)或点阵图,是使用像素阵列(Pixel-array/Dot-matrix点阵)来表示的图像。
位图的像素都分配有特定的位置和颜色值。
每个像素的颜色信息由RGB组合或者灰度值表示。
根据位深度可将位图分为1、4、8、16、24及32位(bite)(https://baike.baidu.com/item/%E4%BD%8D)图像等。每个像素使用的信息位数越多,可用的颜色就越多,颜色表现就越逼真,相应的数据量越大。例如,位深度为 1 的像素位图只有两个可能的值(黑色和白色),所以又称为二值位图。位深度为 8 的图像有 2(即 256)个可能的值。位深度为 8 的灰度模式图像有 256 个可能的灰色值。
RGB图像由三个颜色通道组成。8 位/通道的 RGB 图像中的每个通道有 256 个可能的值,这意味着该图像有 1600 万个以上可能的颜色值。有时将带有 8 位/通道 (bpc) 的 RGB 图像称作 24 位图像(8 位 x 3 通道 = 24 位数据/像素)。通常将使用24位RGB组合数据位表示的的位图称为真彩色位图。

内容摘自百度百科。
由上面的描述可知,我们可以将bitmap理解为一个点阵图或者是一个数组,其中的每个元素都是一个像素信息,假设对于一个32位RGBA图像来说,则每个元素包含着三个颜色组件(R,G,B)和一个Alpha组件,每一个组件占8位(8bite = 1byte = 32 / 4)。这些像素集合起来就可以表示出一张图片。


iOS中的Bitmap

在iOS中,Bitmap的数据由CGImageRef封装。由以下几个函数可以创建CGImageRef对象

CGImageCreate - 最灵活,但也是最复杂的一种方式,要传入11个参数,这个方法最后讲解。
CGImageSourceCreate-ImageAtIndex-通过已经存在的Image对象来创建
CGImageSourceCreate-ThumbnailAtIndex- 和上一个函数类似,不过这个是创建缩略图
CGBitmapContextCreateImage - 通过Copy Bitmap Graphics来创建
CGImageCreateWith-ImageInRect -通过在某一个矩形内数据来创建

如果要使用bitmap对图片进行各种处理,则需要先创建位图上下文。(CGBitmapContextCreate,Swift中则是CGContext)
先看一下初始化方法:

CGContextRef __nullable CGBitmapContextCreate(void * __nullable data,
                                              size_t width,
                                              size_t height,
                                              size_t bitsPerComponent,
                                              size_t bytesPerRow,
                                              CGColorSpaceRef space,
                                              uint32_t bitmapInfo)

下面是一个例子:

    let w = Int(image.size.width)
    let h = Int(image.size.height)
    let bitsPerComponent = 8
    let bytesPerRow = w * 4
    let colorSpace = CGColorSpaceCreateDeviceRGB()
    let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue
    let bufferData = UnsafeMutablePointer.allocate(capacity: w * h)
    bufferData.initialize(repeating: 0, count: w * h)
    let cxt = CGContext(data: bufferData,
                                width: w,
                                height: h,
                                bitsPerComponent: bitsPerComponent,
                                bytesPerRow: bytesPerRow,
                                space: colorSpace,
                                bitmapInfo: bitmapInfo)

参数说明

  • data:
    用于存放位图的点阵数据,当生成上下文并调用 CGContextDrawImage 方法将指定图片绘制进上下文之后,data里面就会有该图片的位图像素信息,可以当做一个数组指针来使用。
    我们可以对这个data里面的内容进行操作,然后以这个data为主要参数通过生成 CGDataProvider 实例并调用 CGImageCreate 方法来重新生成一个CGImage。

  • width 和 height:
    位图的宽和高。
    如width = 10,height = 20则代表每一行有10个像素,每一列有20个像素。

  • bitsPerComponent:
    颜色组件或者alpha组件占的bite数。
    以32位图像为例:bitsPerComponent = 8

  • bytesPerRow:
    位图的每一行占的字节数。
    以32位图像为例:一个像素有4byte(rgba),
    那么bytesPerRow = width * 4

  • space:
    颜色空间,是RGBA、CMYK还是灰度值。
    RGBA : CGColorSpaceCreateDeviceRGB( )
    CMYK : CGColorSpaceCreateDeviceCMYK( )
    灰度值 : CGColorSpaceCreateDeviceGray( )

  • bitmapInfo:
    一个常量,描述这个位图上下文所对应的位图的基本信息。
    通常是多个枚举值做或运算的最终值(CGBitmapInfo 和 CGImageAlphaInfo)。
    比如可以置顶是否具有alpha通道,alpha通道的位置(是RGBA还是ARGB),字节排列的顺序等等。


CGBitmapInfo 和 CGImageAlphaInfo

public struct CGBitmapInfo : OptionSet {
    public init(rawValue: UInt32)   
    public static var alphaInfoMask: CGBitmapInfo { get }
    public static var floatInfoMask: CGBitmapInfo { get }
    public static var floatComponents: CGBitmapInfo { get }    
    public static var byteOrderMask: CGBitmapInfo { get }   
    public static var byteOrder16Little: CGBitmapInfo { get }
    public static var byteOrder32Little: CGBitmapInfo { get }
    public static var byteOrder16Big: CGBitmapInfo { get }
    public static var byteOrder32Big: CGBitmapInfo { get }
}
public enum CGImageAlphaInfo : UInt32 {
    case none /* For example, RGB. */
    case premultipliedLast /* For example, premultiplied RGBA */
    case premultipliedFirst /* For example, premultiplied ARGB */
    case last /* For example, non-premultiplied RGBA */
    case first /* For example, non-premultiplied ARGB */
    case noneSkipLast /* For example, RBGX. */
    case noneSkipFirst /* For example, XRGB. */
    case alphaOnly /* No color data, alpha data only */
}

上面的是 CGBitmapInfo 和 CGImageAlphaInfo 的定义。这里面有几个关键点需要说明一下:
· Last和First:
...Last 代表alpha分量在末尾即RGBA。那么解析颜色和alpha分量时为下面的顺序:

        let r = CGFloat((pixel >> 0)  & 0xff) / 255.0
        let g = CGFloat((pixel >> 8)  & 0xff) / 255.0
        let b = CGFloat((pixel >> 16) & 0xff) / 255.0
        let a = CGFloat((pixel >> 24) & 0xff) / 255.0
        let color = UIColor(displayP3Red: r, green: g, blue: b, alpha: 1)

...First 代表alpha分量在开头即ARGB。那么解析颜色和alpha分量时为下面的顺序:

        let a = CGFloat((pixel >> 0)  & 0xff) / 255.0
        let r = CGFloat((pixel >> 8)   & 0xff) / 255.0
        let g = CGFloat((pixel >> 16) & 0xff) / 255.0
        let b = CGFloat((pixel >> 24) & 0xff) / 255.0
        let color = UIColor(displayP3Red: r, green: g, blue: b, alpha: 1)

· premultiplied 预乘透明度:
比如常规的半透明图像的RGBA归一化值为(1, 0.5, 0.5, 0.5),如果做了预乘透明度的话,那么RGBA的归一化值则为(1 * 0.5, 0.5 * 0.5, 0.5*0.5, 0.5) = (1, 0.25, 0.25, 0.5) ,即每个颜色分量都乘以alpha通道值作为结果值。

color.rgb *= color.alpha

所以带有 premultiplied时,说明在图片解码压缩的时候,就将 alpha 通道的值分别乘到了颜色分量上,我们知道 alpha 就会影响颜色的透明度,我们如果在压缩的时候就将这步做完了,那么渲染的时候就不必再处理 alpha 通道了,这样在显示位图的时候直接显示就行了,这样就提高了性能。
因此,如果指明了 bitmapInfo 为 premultipliedFirst 或者 premultipliedLast 的话,生成位图上下文后,解析出来的rgb的颜色值是乘以alpha之后的值。

那么如果我不想预乘透明度,只想获取原始的rgb颜色色值呢?直接指定bitmapInfo为 last 或者 first 就可以了吧?如下:

    let w = 1
    let h = 1
    let bitsPerComponent = 8
    let bytesPerRow = w * 4
    let colorSpace = CGColorSpaceCreateDeviceRGB()
    let bitmapInfo = CGImageAlphaInfo.last.rawValue 
    var bufferData = Array(repeating: 0, count: 1)

    guard let cxt = CGContext(data: &bufferData,
                              width: w,
                              height: h,
                              bitsPerComponent: bitsPerComponent,
                              bytesPerRow: bytesPerRow,
                              space: colorSpace,
                              bitmapInfo: bitmapInfo)
    else {
        return nil
    }

这时我们运行会发现生成的位图上下文 cxt 为 nil,并且控制台输出了下面的错误信息:


BubblePopAnimationDemo[24851:2078573] [Unknown process name] CGBitmapContextCreate: unsupported parameter combination: set CGBITMAP_CONTEXT_LOG_ERRORS environmental variable to see the details

这时我们可以不使用 .last 或者 .first,而使用 .noneSkipLast 或者 .noneSkipFirst。
"noneSkip" 代表有 alpha 分量,但是忽略该值,相当于透明度不起作用。
所以如果指定 bitmapInfo 为 .noneSkipFirst 或者 .noneSkipLast,就不会出现异常,并且我们最后就可以解析出原始的没有预乘alpha的rgb颜色值了。

· CGBitmapInfo
CGBitmapInfo 是一个枚举几何,用来描述一个位图的基本信息。

// 获取一个图片的位图信息
let bitmapInfo = image.cgImage.bitmapInfo

在我们创建位图上下文或者CGImage指定bitmapInfo时,如果想要使用CGBitmapInfo,通常是将它的值和CGImageAlphaInfo做按位或运算。如下:

CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue

下面是苹果帮助文档对 CGBitmapInfo 的概括。

Applications that store pixel data in memory using ARGB format must take care in how they read data. If the code is not written correctly, it’s possible to misread the data which leads to colors or alpha that appear wrong. The byte order constants specify the byte ordering of pixel formats. To specify byte ordering, use a bitwise OR operator to combine the appropriate constant with the bitmapInfo parameter.

大概意思是,存储在内存中的ARGB格式的像素数据必须要注意读取这个数据的方式。如果没有正确地读取,会导致颜色和透明度显示错误。
byte order常量标识这一个像素中各个分量的字节排列方式。如果想要指明字节排列方式,需要将bitmapInfo中的各个值以按位或的方式组合起来。

属性 说明
alphaInfoMask 用来标识位图是否有alpha通道
floatComponents 位图中的各个组件分量的值是否为浮点值
byteOrderMask 像素的字节排序格式
byteOrder16Little 16位图像以小端对其方式排列字节分量
byteOrder32Little 32位图像以小端对其方式排列字节分量
byteOrder16Big 16位图像以大端对其方式排列字节分量
byteOrder32Big 32位图像以大端对其方式排列字节分量
floatInfoMask 没有说明

下面的例子是输出一个图片的上述的几个位图信息:

-(void)imageDump:(NSString*)file  
{  
    UIImage* image = [UIImage imageNamed:file];  
    CGImageRef cgimage = image.CGImage;  
    size_t width  = CGImageGetWidth(cgimage);  
    size_t height = CGImageGetHeight(cgimage);  
    size_t bpr = CGImageGetBytesPerRow(cgimage);  
    size_t bpp = CGImageGetBitsPerPixel(cgimage);  
    size_t bpc = CGImageGetBitsPerComponent(cgimage);  
    size_t bytes_per_pixel = bpp / bpc;  
    CGBitmapInfo info = CGImageGetBitmapInfo(cgimage);  
  
    NSLog(  
        @"\n"  
        "===== %@ =====\n"  
        "CGImageGetHeight: %d\n"  
        "CGImageGetWidth:  %d\n"  
        "CGImageGetColorSpace: %@\n"  
        "CGImageGetBitsPerPixel:     %d\n"  
        "CGImageGetBitsPerComponent: %d\n"  
        "CGImageGetBytesPerRow:      %d\n"  
        "CGImageGetBitmapInfo: 0x%.8X\n"  
        "  kCGBitmapAlphaInfoMask     = %s\n"  
        "  kCGBitmapFloatComponents   = %s\n"  
        "  kCGBitmapByteOrderMask     = %s\n"  
        "  kCGBitmapByteOrderDefault  = %s\n"  
        "  kCGBitmapByteOrder16Little = %s\n"  
        "  kCGBitmapByteOrder32Little = %s\n"  
        "  kCGBitmapByteOrder16Big    = %s\n"  
        "  kCGBitmapByteOrder32Big    = %s\n",  
        file,  
        (int)width,  
        (int)height,  
        CGImageGetColorSpace(cgimage),  
        (int)bpp,  
        (int)bpc,  
        (int)bpr,  
        (unsigned)info,  
        (info & kCGBitmapAlphaInfoMask)     ? "YES" : "NO",  
        (info & kCGBitmapFloatComponents)   ? "YES" : "NO",  
        (info & kCGBitmapByteOrderMask)     ? "YES" : "NO",  
        (info & kCGBitmapByteOrderDefault)  ? "YES" : "NO",  
        (info & kCGBitmapByteOrder16Little) ? "YES" : "NO",  
        (info & kCGBitmapByteOrder32Little) ? "YES" : "NO",  
        (info & kCGBitmapByteOrder16Big)    ? "YES" : "NO",  
        (info & kCGBitmapByteOrder32Big)    ? "YES" : "NO"  
    );  
  
    CGDataProviderRef provider = CGImageGetDataProvider(cgimage);  
    NSData* data = (id)CGDataProviderCopyData(provider);  
    [data autorelease];  
    const uint8_t* bytes = [data bytes];  
  
    printf("Pixel Data:\n");  
    for(size_t row = 0; row < height; row++)  
    {  
        for(size_t col = 0; col < width; col++)  
        {  
            const uint8_t* pixel =  
                &bytes[row * bpr + col * bytes_per_pixel];  
  
            printf("(");  
            for(size_t x = 0; x < bytes_per_pixel; x++)  
            {  
                printf("%.2X", pixel[x]);  
                if( x < bytes_per_pixel - 1 )  
                    printf(",");  
            }  
  
            printf(")");  
            if( col < width - 1 )  
                printf(", ");  
        }  
  
        printf("\n");  
    }  
}  

这里说一下byteOrder和alphaInfo中last和first之间的搭配。
byteOrderXXLittle : 生成的信息位置为倒序
byteOrderXXBig : 生成的信息位置为顺序
XXXXFirst : ARGB
XXXXLast : RGBA

属性 结果
.premultipliedFirst + .byteOrder32Big A R G B
.premultipliedLast + .byteOrder32Big R G B A
.premultipliedFirst + .byteOrder32Little R G B A
.premultipliedLast + .byteOrder32Little A R G B

两者以按位或运算来得到最终的值。


代码实践

终于到了实践的环节了,在阅读了几个大神的博客之后,以他们的代码为原型加了些自己的想法和说明。如果想看原贴,大家可以直接跳到最后的参考部分。

  • 获取图片中点击位置的颜色:
  1. 获取imageView控件bounds范围内像素数据。
  2. 通过CGPoint的参数计算出该点对应的像素的索引位置并取出像素数据。
  3. 逐个字节解析出rgb颜色分量和alpha分量值最后生成UIColor最为结果返回。
extension UIImageView {
    
    func color(forPoint p : CGPoint) -> UIColor? {
        guard let pixels = self.pixels else {
            return nil
        }
        guard let index = pixelIndex(for: p) else {
            return nil
        }
        let color = self.color(forPixel: pixels[index])
        return color
    }

    /*
     获取图片的像素数据
     */
    var pixels : [UInt32]? {
        return self.getPixelsData(inRect: self.bounds)
    }
    /*
     根据坐标点获取该点对应的像素所在数组中的索引
     - p : 置顶的坐标点
     */
    func pixelIndex(for p : CGPoint) -> Int? {
        let size = self.bounds.size
        guard p.x > 0 && p.x <= size.width && p.y > 0 && p.y < size.height else {
            return nil
        }
        // 相当于 height * bytesPerRow + x
        let floatIndex = Int(size.width * p.y + p.x)
        let intIndex = Int(size.width) * Int(p.y) + Int(p.x)
        print("float index : \(floatIndex), intIndex : \(intIndex)")
        // 这里一定要都转换成Int类型再求值,否则最后算出来的index会有偏差
        return Int(size.width) * Int(p.y) + Int(p.x)
    }
    
    func color(forPixel pixel: UInt32) -> UIColor {
        // 创建位图上下文的时候,可以指定两种bitmapInfo
        // 如果指定了premultipliedFirst,说明颜色组件是以 alpha red green blue 的顺序排列的
        // 如果指定了premultipliedLast,说明颜色组件是以 red green blue alpha 的顺序排列的
        // 那么下面解析r,g,b,a四个值的时候的顺序就会有所差别。
        let r = CGFloat((pixel >> 0)  & 0xff) / 255.0
        let g = CGFloat((pixel >> 8)  & 0xff) / 255.0
        let b = CGFloat((pixel >> 16) & 0xff) / 255.0
        let a = CGFloat((pixel >> 24) & 0xff) / 255.0
        print("r : \(r), g : \(g), b : \(b), a : \(a)")
        let color = UIColor(displayP3Red: r, green: g, blue: b, alpha: 1)
        return color
    }
    
    /*
     获取图片中指定范围内的位图数据(rgba数组)
     - rect : 置顶要获取像素数组的范围
     生成rect范围内的像素数据,较为耗时,所以在真正使用的时候最好有缓存策略。
     */
    func getPixelsData(inRect rect : CGRect) -> [UInt32]? {
        
        guard let img = self.image, let cgImg = img.cgImage else {
            return nil
        }
        /*
         不能直接以image的宽高作为绘制的宽高,因为image的size可能会比控件的size大很多。
         所以在生成bitmapContext的时候需要以实际的控件宽高为准
         */
        let w = Int(rect.size.width)
        let h = Int(rect.size.height)
        let bitsPerComponent = 8 // 32位的图像,所以每个颜色组件包含8bit
        let bytesPerRow = w * 4  // 1 byte = 8 bit, 32位图像的话,每个像素包含4个byte
        let colorSpace = CGColorSpaceCreateDeviceRGB()
        let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue // RGBA
        // let bitmapInfo = CGImageAlphaInfo.premultipliedFirst.rawValue // ARGB
        // 因为是32位图像,RGBA各占8位 8*4=32,所以像素数据的数组的元素类型应该是UInt32。
        var bufferData = Array(repeating: 0, count: w * h)
        guard let cxt = CGContext(data: &bufferData, width: w, height: h, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo) else {
            return nil
        }
        // 将图像绘制进上下文中
        cxt.draw(cgImg, in: rect)
        return bufferData
    }
}

【注意1】
步骤1获取图像的像素数据(getPixelsData)是耗时操作。
在实际使用时应该在获取之后进行缓存,以备之后重复使用。

【注意2】
代码中计算指定像素的索引位置时,要把各个计算元素都先转换成Int类型之后再计算,如果以CGFloat类型计算完再转换成Int类型的话,得到的索引值会有偏差,导致获取到的颜色不正确。
Int(size.width * p.y + p.x) --> 错误
Int(size.width) * Int(p.y) + Int(p.x) --> 正确


  • 另一种思路 获取图片中点击位置的颜色:
  1. 生成只获取容纳一个像素的 BitmapContex。
  2. 根据 p 点的位置对 BitmapContext 进行平移变换,使 BitmapContext 的绘制原点位于 p 点。(默认渲染原点是在左上角)
    /*
     另一种思路获取点击位置的颜色。上面的getPixelsData需要获取整张图片的像素数据,
     对于只想要取得某一个点位置的颜色来说,效率较低。所以只生成容纳一个像素的bitmap,
     然后直接根据bufferData中像素数据生成颜色并返回
     */
    func getColor(fromPoint p : CGPoint) -> UIColor? {
        
        let w = 1
        let h = 1
        let bitsPerComponent = 8
        let bytesPerRow = w * 4
        let colorSpace = CGColorSpaceCreateDeviceRGB()
        let bitmapInfo = CGBitmapInfo.byteOrder32Big.rawValue | CGImageAlphaInfo.noneSkipLast.rawValue // RGBA
        // 可以声明为一个有1个元素的UInt32数组
        var bufferData = Array(repeating: 0, count: 1)
        // 或者为一个有4个元素的UInt8数组
        // var bufferData = Array(repeating: 0, count: 4)

        guard let cxt = CGContext(data: &bufferData, width: w, height: h, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo) else {
            return nil
        }
        /*
         这里需要注意,由于上边生成的位图上下文只包含一个像素数据,相当于一个点。
         而这个位图上下文的默认渲染原点是图片的左上角,也就是(0,0)的位置,如果直接从bufferData获取的话,其实是图片左上角第一个像素的颜色。
         所以这里需要将位图上下文做一个反方向的平移变换,使p点成为位图上下文的渲染原点
         */
        cxt.translateBy(x: -p.x, y: -p.y)
        
        /*
         将图像渲染到上下文中,这里需要注意的是,需要在平移之后才渲染,否则获取到的颜色不正确。
         */
        layer.render(in: cxt)
        
        // 只包含一个UInt32像素数据
        let component = bufferData.first!
        let r = CGFloat((component >> 0)  & 0xff) / 255.0
        let g = CGFloat((component >> 8)  & 0xff) / 255.0
        let b = CGFloat((component >> 16) & 0xff) / 255.0
        let a = CGFloat((component >> 24) & 0xff) / 255.0
        
        // 包含四个UInt8(每一个元素代表RGBA中的一个)元素的数组
        // let r = CGFloat(bufferData[0]) / 255.0
        // let g = CGFloat(bufferData[1]) / 255.0
        // let b = CGFloat(bufferData[2]) / 255.0
        // let a = CGFloat(bufferData[3]) / 255.0

        let color = UIColor(displayP3Red: r, green: g, blue: b, alpha: a)
        return color
    }
  • 将图像中最多的颜色替换成另一种颜色:
  1. 根据指定图片生成位图上下文,调用 draw 方法之后可以获取到这个图片的像素数据(bufferData)。
  2. 生成一个小尺寸的位图上下文,统计这个上下文的像素数据中出现次数最多的rgba并返回(getMaxCountColor 方法)
  3. 遍历 1 中生成的每一个像素,如果rgb三个颜色值的偏差值小于我们所指定的偏差值(leeway),就将这个像素的颜色改为我们所要指定的rgb颜色。
  4. 将修改颜色之后的像素数据(bufferData)当做参数生成CGDataProvider 实例,然后以 CGDataProvider 实例为参数生成 CGImage 实例。
  5. 最后通过** CGImage** 实例生成修改了像素颜色的 UIImage 实例并返回。
/*
 将图像中出现次数最多的颜色修改为置顶的颜色
 */
func changeMaxCountColorToColor(withRed red : Int,
                                green : Int,
                                blue : Int,
                                alpha : CGFloat,
                                leeway : Float,
                                sourceImage : UIImage?) -> UIImage? {
    guard let image = sourceImage, let cgImage = image.cgImage else {
        return nil
    }
    let w = Int(image.size.width)
    let h = Int(image.size.height)
    let bitsPerComponent = 8
    let bytesPerRow = w * 4
    let colorSpace = CGColorSpaceCreateDeviceRGB()
    let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue
    // var bufferData = Array(repeating: 0, count: w * h)
    // var bufferData = [UInt32](repeatElement(0, count: w*h))
    let bufferData = UnsafeMutablePointer.allocate(capacity: w * h * 4)
    bufferData.initialize(repeating: 0, count: w * h)
    
    guard let cxt = CGContext(data: bufferData, width: w, height: h, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo) else {
        return nil
    }
    cxt.draw(cgImage, in: CGRect(x: 0, y: 0, width: CGFloat(w), height: CGFloat(h)))
    guard let maxC = getMaxCountColor(image) else {
        return nil
    }
    for i in 0 ..< w * h {
        let byteStart = i * 4
        let r = Float(bufferData.advanced(by: byteStart).pointee)
        let g = Float(bufferData.advanced(by: byteStart + 1).pointee)
        let b = Float(bufferData.advanced(by: byteStart + 2).pointee)
        if abs(Float(maxC.r)-r) < leeway && abs(Float(maxC.g)-g) < leeway && abs(Float(maxC.b)-b) < leeway {
            bufferData.advanced(by: byteStart).pointee = UInt8(red)
            bufferData.advanced(by: byteStart+1).pointee = UInt8(green)
            bufferData.advanced(by: byteStart+2).pointee = UInt8(blue)
            //传进来的alpha是归一化的值,所以这里需要除以255.0
            bufferData.advanced(by: byteStart+3).pointee = UInt8(alpha * 255.0)
        }
    }
    let dataProvider = CGDataProvider(dataInfo: nil, data: bufferData, size: bytesPerRow * h) {
        (_, data, _) in
        data.deallocate()
    }
    guard let provider = dataProvider else {
        return nil
    }
    
    let cgBitmapInfoUInt32 = CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue
    let cgBitmapInfo = CGBitmapInfo(rawValue: cgBitmapInfoUInt32)
    let newCGImageOptional = CGImage(width: w, height: h,
                                     bitsPerComponent: 8,
                                     bitsPerPixel: 32,
                                     bytesPerRow: bytesPerRow,
                                     space: colorSpace,
                                     bitmapInfo: cgBitmapInfo,
                                     provider: provider,
                                     decode: nil,
                                     shouldInterpolate: true,
                                     intent: CGColorRenderingIntent.defaultIntent)
    guard let newCGImage = newCGImageOptional else {
        return nil
    }
    let newImage = UIImage(cgImage: newCGImage)
    // 如果在这里将bufferData的内存释放,那么会导致新图片赋值到imageView.image之后看不到图片
    // 应该在创建 CGDataProvider 时的回调函数里面释放
    // bufferData.deinitialize(count: w*h)
    // bufferData.deallocate()
    return newImage
}

/*
 获取出现次数最多的rgba
 */
func getMaxCountColor(_ image : UIImage?) -> (r : Int, g : Int, b : Int, a : Int, pixelColor : UInt32)? {
    guard let image = image, let cgImage = image.cgImage else {
        return nil
    }
    // 先把图片缩小 加快计算速度. 但越小结果误差可能越大
    let w = 150
    let h = 150
    let bitsPerComponent = 8
    let bytesPerRow = w * 4
    let colorSpace = CGColorSpaceCreateDeviceRGB()
    let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue // RGBA
    var bufferData = Array(repeating: 0, count: w * h)
    guard let cxt = CGContext(data: &bufferData, width: w, height: h, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo) else {
        return nil
    }
    cxt.draw(cgImage, in: CGRect(x: 0, y: 0, width: CGFloat(w), height: CGFloat(h)))
    var colorCountDic = [UInt32 : Int]()
    var maxCountColor : UInt32 = 0
    let colorNum = w * h
    for i in 0 ..< colorNum {
        let color = bufferData[i]
        if let count = colorCountDic[color] {
            colorCountDic[color] = count + 1
        } else {
            colorCountDic[color] = 1
        }
        if let maxColorCount = colorCountDic[maxCountColor] {
            if colorCountDic[color]! > maxColorCount {
                maxCountColor = color
            }
        } else {
            maxCountColor = color
        }
    }
    let r = Int((maxCountColor >> 0)  & 0xff)
    let g = Int((maxCountColor >> 8)  & 0xff)
    let b = Int((maxCountColor >> 16) & 0xff)
    let a = Int((maxCountColor >> 24) & 0xff)
    return (r,g,b,a, maxCountColor)
}

/*
 将图像中出现次数最多的颜色变为透明色
 */
func changeMaxCountColorToTransparent(leeway : Float, image : UIImage?) -> UIImage? {
    return changeMaxCountColorToColor(withRed: 0, green: 0, blue: 0, alpha: 0, leeway: 10, sourceImage: image)
}

【注意1】
bufferData 的生成方式和上面的例子不同了。上面的 getPixelsData 方法里面 bufferData 是一个 Swift 数组,在从图像中获取像素数据之后直接使用即可。

但是在这个例子里面 bufferData 是 UnsafeMutablePointer 类型。
原因是如果 bufferData 是Swift数组的话,在后面生成 CGDataProvider 实例并最终生成 UIImage 之后,放在控件上显示不出来。

UnsafeMutablePointer 类型相当于C语言的数组指针,即C语言的数组。若想使用该类型的变量的话需要自己创建并分配内存和释放。具体可以参考: https://blog.csdn.net/zkh90644/article/details/52819002

还有一点需要注意的是,如果使用 UnsafeMutablePointer 变量,就不需要在传参的时候写上取地址符(&bufferData)了,如果是普通的Swift数组的话则需要加上

【注意2】
这里我使用了 UInt8 数组来存储像素(上面的例子里面是UInt32数组),
原因是为了方便像素数据中颜色分量的重新赋值,而不用做各种按位与和或的运算。

    for i in 0 ..< w * h {
        let byteStart = i * 4
        let r = Float(bufferData.advanced(by: byteStart).pointee)
        let g = Float(bufferData.advanced(by: byteStart + 1).pointee)
        let b = Float(bufferData.advanced(by: byteStart + 2).pointee)
        if abs(Float(maxC.r)-r) < leeway && abs(Float(maxC.g)-g) < leeway && abs(Float(maxC.b)-b) < leeway {
            bufferData.advanced(by: byteStart).pointee = UInt8(red)
            bufferData.advanced(by: byteStart+1).pointee = UInt8(green)
            bufferData.advanced(by: byteStart+2).pointee = UInt8(blue)
            //传进来的alpha是归一化的值,所以这里需要除以255.0
            bufferData.advanced(by: byteStart+3).pointee = UInt8(alpha * 255.0)
        }
    }

【注意3】
修改完颜色并根据像素数据生成CGImage的时候也要指明bitmapInfo,
因为最终的图片可以是半透明的,即我们需要alpha通道生效。
所以此时应该指明 CGImageAlphaInfo.premultipliedLast 或者 .premultipliedFirst

  • 设置图片透明度
func setAlpha(_ alpha : CGFloat, sourceImage : UIImage?) -> UIImage? {
    guard let img = sourceImage, let cgImg = img.cgImage else {
        return nil
    }
    UIGraphicsBeginImageContextWithOptions(img.size, false, 1)
    
    guard let cxt = UIGraphicsGetCurrentContext() else {
        return nil
    }
    // 调用draw方法之后,图片的downMirror的,所以这里需要提前做一下反转和平移变换
    cxt.scaleBy(x: 1, y: -1)
    cxt.translateBy(x: 0, y: -img.size.height)
    cxt.setBlendMode(CGBlendMode.multiply)
    cxt.setAlpha(alpha)
    cxt.draw(cgImg, in: CGRect(x: 0, y: 0, width: img.size.width, height: img.size.height))
    let newImage = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    return newImage
}
  • 二分法压缩图片大小:
    static func compress2Data(_ comressImage: UIImage,
                              limitBytes maxBytesLength: Int) -> Data {
        var max: CGFloat = 1
        var min: CGFloat = 0
        var compression: CGFloat = 1
        var compressedData: Data! = nil
        for _ in 0 ..< 6 {
            compression = (max + min) / 2
            compressedData = comressImage.jpegData(compressionQuality: compression)!
            if CGFloat(compressedData.count) < CGFloat(maxBytesLength) * 0.9 {
                min = compression
            } else if compressedData.count > maxBytesLength {
                max = compression
            } else {
                break
            }
        }
        return compressedData
    }
  • 获取图片格式:
public enum ImageFormatType {
    case jpg
    case png
    case gif
    case webP
    case unknown
}

public struct ImageHeaderData {
    static var PNG: [UInt8] = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]
    static var JPEG_SOI: [UInt8] = [0xFF, 0xD8]
    static var JPEG_IF: [UInt8] = [0xFF]
    static var GIF: [UInt8] = [0x47, 0x49, 0x46]
}

extension Data {
    public var imageFormatType: ImageFormatType {
        var buffer = [UInt8](repeating: 0, count: 8)
        (self as NSData).getBytes(&buffer, length: 8)
        if buffer == ImageHeaderData.PNG {
            return .png
        } else if buffer[0] == ImageHeaderData.JPEG_SOI[0] &&
            buffer[1] == ImageHeaderData.JPEG_SOI[1] &&
            buffer[2] == ImageHeaderData.JPEG_IF[0]
        {
            return .jpg
        } else if buffer[0] == ImageHeaderData.GIF[0] &&
            buffer[1] == ImageHeaderData.GIF[1] &&
            buffer[2] == ImageHeaderData.GIF[2]
        {
            return .gif
        }
        
        if count < 12 {
            return .unknown
        }
        
        let endIndex = index(startIndex, offsetBy: 12)
        let testData = subdata(in: startIndex..

以上就是我最近所总结的图片和Bitmap相关的知识,这期间参考了很多朋友的博客,学到了很多也给了我很多的思路,非常感谢。如果以后学到了更多相关知识还会继续更新。

参考:
https://blog.csdn.net/rpf2014/article/details/52598280
https://www.jianshu.com/p/12d0ec666959
https://cloud.tencent.com/developer/ask/127227
https://blog.csdn.net/hello_hwc/article/details/49614263
https://blog.csdn.net/jeffasd/article/details/80571366
https://www.jianshu.com/p/52e6fec1b418
https://blog.csdn.net/jeffasd/article/details/78142067

你可能感兴趣的:(【iOS】Bitmap 位图详解与实践)