在工作中經(jīng)常會(huì)遇到要對(duì)UIImage做各種處理,如旋轉(zhuǎn)、放大縮小、裁剪等等,更深入的則可能會(huì)對(duì)圖片上的像素進(jìn)行操作。最近更深入地了解了一下位圖(Bitmap)的相關(guān)知識(shí)。下面文章主要分為一下幾個(gè)部分:
- 了解Bitmap
- iOS中的Bitmap
- 代碼實(shí)踐
了解Bitmap
位圖(Bitmap),又稱柵格圖(英語(yǔ):Raster graphics)或點(diǎn)陣圖,是使用像素陣列(Pixel-array/Dot-matrix點(diǎn)陣)來(lái)表示的圖像。
位圖的像素都分配有特定的位置和顏色值。
每個(gè)像素的顏色信息由RGB組合或者灰度值表示。
根據(jù)位深度可將位圖分為1、4、8、16、24及32位(bite)(https://baike.baidu.com/item/%E4%BD%8D)圖像等。每個(gè)像素使用的信息位數(shù)越多,可用的顏色就越多,顏色表現(xiàn)就越逼真,相應(yīng)的數(shù)據(jù)量越大。例如,位深度為 1 的像素位圖只有兩個(gè)可能的值(黑色和白色),所以又稱為二值位圖。位深度為 8 的圖像有 2(即 256)個(gè)可能的值。位深度為 8 的灰度模式圖像有 256 個(gè)可能的灰色值。
RGB圖像由三個(gè)顏色通道組成。8 位/通道的 RGB 圖像中的每個(gè)通道有 256 個(gè)可能的值,這意味著該圖像有 1600 萬(wàn)個(gè)以上可能的顏色值。有時(shí)將帶有 8 位/通道 (bpc) 的 RGB 圖像稱作 24 位圖像(8 位 x 3 通道 = 24 位數(shù)據(jù)/像素)。通常將使用24位RGB組合數(shù)據(jù)位表示的的位圖稱為真彩色位圖。
內(nèi)容摘自百度百科。
由上面的描述可知,我們可以將bitmap理解為一個(gè)點(diǎn)陣圖或者是一個(gè)數(shù)組,其中的每個(gè)元素都是一個(gè)像素信息,假設(shè)對(duì)于一個(gè)32位RGBA圖像來(lái)說(shuō),則每個(gè)元素包含著三個(gè)顏色組件(R,G,B)和一個(gè)Alpha組件,每一個(gè)組件占8位(8bite = 1byte = 32 / 4)。這些像素集合起來(lái)就可以表示出一張圖片。
iOS中的Bitmap
在iOS中,Bitmap的數(shù)據(jù)由CGImageRef封裝。由以下幾個(gè)函數(shù)可以創(chuàng)建CGImageRef對(duì)象
CGImageCreate - 最靈活,但也是最復(fù)雜的一種方式,要傳入11個(gè)參數(shù),這個(gè)方法最后講解。
CGImageSourceCreate-ImageAtIndex-通過(guò)已經(jīng)存在的Image對(duì)象來(lái)創(chuàng)建
CGImageSourceCreate-ThumbnailAtIndex- 和上一個(gè)函數(shù)類似,不過(guò)這個(gè)是創(chuàng)建縮略圖
CGBitmapContextCreateImage - 通過(guò)Copy Bitmap Graphics來(lái)創(chuàng)建
CGImageCreateWith-ImageInRect -通過(guò)在某一個(gè)矩形內(nèi)數(shù)據(jù)來(lái)創(chuàng)建
如果要使用bitmap對(duì)圖片進(jìn)行各種處理,則需要先創(chuàng)建位圖上下文。(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)
下面是一個(gè)例子:
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<UInt32>.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)
參數(shù)說(shuō)明
data:
用于存放位圖的點(diǎn)陣數(shù)據(jù),當(dāng)生成上下文并調(diào)用 CGContextDrawImage 方法將指定圖片繪制進(jìn)上下文之后,data里面就會(huì)有該圖片的位圖像素信息,可以當(dāng)做一個(gè)數(shù)組指針來(lái)使用。
我們可以對(duì)這個(gè)data里面的內(nèi)容進(jìn)行操作,然后以這個(gè)data為主要參數(shù)通過(guò)生成 CGDataProvider 實(shí)例并調(diào)用 CGImageCreate 方法來(lái)重新生成一個(gè)CGImage。width 和 height:
位圖的寬和高。
如width = 10,height = 20則代表每一行有10個(gè)像素,每一列有20個(gè)像素。bitsPerComponent:
顏色組件或者alpha組件占的bite數(shù)。
以32位圖像為例:bitsPerComponent = 8bytesPerRow:
位圖的每一行占的字節(jié)數(shù)。
以32位圖像為例:一個(gè)像素有4byte(rgba),
那么bytesPerRow = width * 4space:
顏色空間,是RGBA、CMYK還是灰度值。
RGBA : CGColorSpaceCreateDeviceRGB( )
CMYK : CGColorSpaceCreateDeviceCMYK( )
灰度值 : CGColorSpaceCreateDeviceGray( )bitmapInfo:
一個(gè)常量,描述這個(gè)位圖上下文所對(duì)應(yīng)的位圖的基本信息。
通常是多個(gè)枚舉值做或運(yùn)算的最終值(CGBitmapInfo 和 CGImageAlphaInfo)。
比如可以置頂是否具有alpha通道,alpha通道的位置(是RGBA還是ARGB),字節(jié)排列的順序等等。
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 的定義。這里面有幾個(gè)關(guān)鍵點(diǎn)需要說(shuō)明一下:
· Last和First:
...Last 代表alpha分量在末尾即RGBA。那么解析顏色和alpha分量時(shí)為下面的順序:
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分量在開(kāi)頭即ARGB。那么解析顏色和alpha分量時(shí)為下面的順序:
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 預(yù)乘透明度:
比如常規(guī)的半透明圖像的RGBA歸一化值為(1, 0.5, 0.5, 0.5),如果做了預(yù)乘透明度的話,那么RGBA的歸一化值則為(1 * 0.5, 0.5 * 0.5, 0.5*0.5, 0.5) = (1, 0.25, 0.25, 0.5) ,即每個(gè)顏色分量都乘以alpha通道值作為結(jié)果值。
color.rgb *= color.alpha
所以帶有 premultiplied時(shí),說(shuō)明在圖片解碼壓縮的時(shí)候,就將 alpha 通道的值分別乘到了顏色分量上,我們知道 alpha 就會(huì)影響顏色的透明度,我們?nèi)绻趬嚎s的時(shí)候就將這步做完了,那么渲染的時(shí)候就不必再處理 alpha 通道了,這樣在顯示位圖的時(shí)候直接顯示就行了,這樣就提高了性能。
因此,如果指明了 bitmapInfo 為 premultipliedFirst 或者 premultipliedLast 的話,生成位圖上下文后,解析出來(lái)的rgb的顏色值是乘以alpha之后的值。
那么如果我不想預(yù)乘透明度,只想獲取原始的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<UInt32>(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
}
這時(shí)我們運(yùn)行會(huì)發(fā)現(xiàn)生成的位圖上下文 cxt 為 nil,并且控制臺(tái)輸出了下面的錯(cuò)誤信息:
BubblePopAnimationDemo[24851:2078573] [Unknown process name] CGBitmapContextCreate: unsupported parameter combination: set CGBITMAP_CONTEXT_LOG_ERRORS environmental variable to see the details
這時(shí)我們可以不使用 .last 或者 .first,而使用 .noneSkipLast 或者 .noneSkipFirst。
"noneSkip" 代表有 alpha 分量,但是忽略該值,相當(dāng)于透明度不起作用。
所以如果指定 bitmapInfo 為 .noneSkipFirst 或者 .noneSkipLast,就不會(huì)出現(xiàn)異常,并且我們最后就可以解析出原始的沒(méi)有預(yù)乘alpha的rgb顏色值了。
· CGBitmapInfo
CGBitmapInfo 是一個(gè)枚舉幾何,用來(lái)描述一個(gè)位圖的基本信息。
// 獲取一個(gè)圖片的位圖信息
let bitmapInfo = image.cgImage.bitmapInfo
在我們創(chuàng)建位圖上下文或者CGImage指定bitmapInfo時(shí),如果想要使用CGBitmapInfo,通常是將它的值和CGImageAlphaInfo做按位或運(yùn)算。如下:
CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue
下面是蘋(píng)果幫助文檔對(duì) 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.
大概意思是,存儲(chǔ)在內(nèi)存中的ARGB格式的像素?cái)?shù)據(jù)必須要注意讀取這個(gè)數(shù)據(jù)的方式。如果沒(méi)有正確地讀取,會(huì)導(dǎo)致顏色和透明度顯示錯(cuò)誤。
byte order常量標(biāo)識(shí)這一個(gè)像素中各個(gè)分量的字節(jié)排列方式。如果想要指明字節(jié)排列方式,需要將bitmapInfo中的各個(gè)值以按位或的方式組合起來(lái)。
| 屬性 | 說(shuō)明 |
|---|---|
| alphaInfoMask | 用來(lái)標(biāo)識(shí)位圖是否有alpha通道 |
| floatComponents | 位圖中的各個(gè)組件分量的值是否為浮點(diǎn)值 |
| byteOrderMask | 像素的字節(jié)排序格式 |
| byteOrder16Little | 16位圖像以小端對(duì)其方式排列字節(jié)分量 |
| byteOrder32Little | 32位圖像以小端對(duì)其方式排列字節(jié)分量 |
| byteOrder16Big | 16位圖像以大端對(duì)其方式排列字節(jié)分量 |
| byteOrder32Big | 32位圖像以大端對(duì)其方式排列字節(jié)分量 |
| floatInfoMask | 沒(méi)有說(shuō)明 |
下面的例子是輸出一個(gè)圖片的上述的幾個(gè)位圖信息:
-(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");
}
}
這里說(shuō)一下byteOrder和alphaInfo中l(wèi)ast和first之間的搭配。
byteOrderXXLittle : 生成的信息位置為倒序
byteOrderXXBig : 生成的信息位置為順序
XXXXFirst : ARGB
XXXXLast : RGBA
| 屬性 | 結(jié)果 |
|---|---|
| .premultipliedFirst + .byteOrder32Big | A R G B |
| .premultipliedLast + .byteOrder32Big | R G B A |
| .premultipliedFirst + .byteOrder32Little | R G B A |
| .premultipliedLast + .byteOrder32Little | A R G B |
兩者以按位或運(yùn)算來(lái)得到最終的值。
代碼實(shí)踐
終于到了實(shí)踐的環(huán)節(jié)了,在閱讀了幾個(gè)大神的博客之后,以他們的代碼為原型加了些自己的想法和說(shuō)明。如果想看原貼,大家可以直接跳到最后的參考部分。
- 獲取圖片中點(diǎn)擊位置的顏色:
- 獲取imageView控件bounds范圍內(nèi)像素?cái)?shù)據(jù)。
- 通過(guò)CGPoint的參數(shù)計(jì)算出該點(diǎn)對(duì)應(yīng)的像素的索引位置并取出像素?cái)?shù)據(jù)。
- 逐個(gè)字節(jié)解析出rgb顏色分量和alpha分量值最后生成UIColor最為結(jié)果返回。
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
}
/*
獲取圖片的像素?cái)?shù)據(jù)
*/
var pixels : [UInt32]? {
return self.getPixelsData(inRect: self.bounds)
}
/*
根據(jù)坐標(biāo)點(diǎn)獲取該點(diǎn)對(duì)應(yīng)的像素所在數(shù)組中的索引
- p : 置頂?shù)淖鴺?biāo)點(diǎn)
*/
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
}
// 相當(dāng)于 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)")
// 這里一定要都轉(zhuǎn)換成Int類型再求值,否則最后算出來(lái)的index會(huì)有偏差
return Int(size.width) * Int(p.y) + Int(p.x)
}
func color(forPixel pixel: UInt32) -> UIColor {
// 創(chuàng)建位圖上下文的時(shí)候,可以指定兩種bitmapInfo
// 如果指定了premultipliedFirst,說(shuō)明顏色組件是以 alpha red green blue 的順序排列的
// 如果指定了premultipliedLast,說(shuō)明顏色組件是以 red green blue alpha 的順序排列的
// 那么下面解析r,g,b,a四個(gè)值的時(shí)候的順序就會(huì)有所差別。
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
}
/*
獲取圖片中指定范圍內(nèi)的位圖數(shù)據(jù)(rgba數(shù)組)
- rect : 置頂要獲取像素?cái)?shù)組的范圍
生成rect范圍內(nèi)的像素?cái)?shù)據(jù),較為耗時(shí),所以在真正使用的時(shí)候最好有緩存策略。
*/
func getPixelsData(inRect rect : CGRect) -> [UInt32]? {
guard let img = self.image, let cgImg = img.cgImage else {
return nil
}
/*
不能直接以image的寬高作為繪制的寬高,因?yàn)閕mage的size可能會(huì)比控件的size大很多。
所以在生成bitmapContext的時(shí)候需要以實(shí)際的控件寬高為準(zhǔn)
*/
let w = Int(rect.size.width)
let h = Int(rect.size.height)
let bitsPerComponent = 8 // 32位的圖像,所以每個(gè)顏色組件包含8bit
let bytesPerRow = w * 4 // 1 byte = 8 bit, 32位圖像的話,每個(gè)像素包含4個(gè)byte
let colorSpace = CGColorSpaceCreateDeviceRGB()
let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue // RGBA
// let bitmapInfo = CGImageAlphaInfo.premultipliedFirst.rawValue // ARGB
// 因?yàn)槭?2位圖像,RGBA各占8位 8*4=32,所以像素?cái)?shù)據(jù)的數(shù)組的元素類型應(yīng)該是UInt32。
var bufferData = Array<UInt32>(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
}
// 將圖像繪制進(jìn)上下文中
cxt.draw(cgImg, in: rect)
return bufferData
}
}
【注意1】
步驟1獲取圖像的像素?cái)?shù)據(jù)(getPixelsData)是耗時(shí)操作。
在實(shí)際使用時(shí)應(yīng)該在獲取之后進(jìn)行緩存,以備之后重復(fù)使用。
【注意2】
代碼中計(jì)算指定像素的索引位置時(shí),要把各個(gè)計(jì)算元素都先轉(zhuǎn)換成Int類型之后再計(jì)算,如果以CGFloat類型計(jì)算完再轉(zhuǎn)換成Int類型的話,得到的索引值會(huì)有偏差,導(dǎo)致獲取到的顏色不正確。
Int(size.width * p.y + p.x) --> 錯(cuò)誤
Int(size.width) * Int(p.y) + Int(p.x) --> 正確
- 另一種思路 獲取圖片中點(diǎn)擊位置的顏色:
- 生成只獲取容納一個(gè)像素的 BitmapContex。
- 根據(jù) p 點(diǎn)的位置對(duì) BitmapContext 進(jìn)行平移變換,使 BitmapContext 的繪制原點(diǎn)位于 p 點(diǎn)。(默認(rèn)渲染原點(diǎn)是在左上角)
/*
另一種思路獲取點(diǎn)擊位置的顏色。上面的getPixelsData需要獲取整張圖片的像素?cái)?shù)據(jù),
對(duì)于只想要取得某一個(gè)點(diǎn)位置的顏色來(lái)說(shuō),效率較低。所以只生成容納一個(gè)像素的bitmap,
然后直接根據(jù)bufferData中像素?cái)?shù)據(jù)生成顏色并返回
*/
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
// 可以聲明為一個(gè)有1個(gè)元素的UInt32數(shù)組
var bufferData = Array<UInt32>(repeating: 0, count: 1)
// 或者為一個(gè)有4個(gè)元素的UInt8數(shù)組
// var bufferData = Array<UInt8>(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
}
/*
這里需要注意,由于上邊生成的位圖上下文只包含一個(gè)像素?cái)?shù)據(jù),相當(dāng)于一個(gè)點(diǎn)。
而這個(gè)位圖上下文的默認(rèn)渲染原點(diǎn)是圖片的左上角,也就是(0,0)的位置,如果直接從bufferData獲取的話,其實(shí)是圖片左上角第一個(gè)像素的顏色。
所以這里需要將位圖上下文做一個(gè)反方向的平移變換,使p點(diǎn)成為位圖上下文的渲染原點(diǎn)
*/
cxt.translateBy(x: -p.x, y: -p.y)
/*
將圖像渲染到上下文中,這里需要注意的是,需要在平移之后才渲染,否則獲取到的顏色不正確。
*/
layer.render(in: cxt)
// 只包含一個(gè)UInt32像素?cái)?shù)據(jù)
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
// 包含四個(gè)UInt8(每一個(gè)元素代表RGBA中的一個(gè))元素的數(shù)組
// 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
}
- 將圖像中最多的顏色替換成另一種顏色:
- 根據(jù)指定圖片生成位圖上下文,調(diào)用 draw 方法之后可以獲取到這個(gè)圖片的像素?cái)?shù)據(jù)(bufferData)。
- 生成一個(gè)小尺寸的位圖上下文,統(tǒng)計(jì)這個(gè)上下文的像素?cái)?shù)據(jù)中出現(xiàn)次數(shù)最多的rgba并返回(getMaxCountColor 方法)
- 遍歷 1 中生成的每一個(gè)像素,如果rgb三個(gè)顏色值的偏差值小于我們所指定的偏差值(leeway),就將這個(gè)像素的顏色改為我們所要指定的rgb顏色。
- 將修改顏色之后的像素?cái)?shù)據(jù)(bufferData)當(dāng)做參數(shù)生成CGDataProvider 實(shí)例,然后以 CGDataProvider 實(shí)例為參數(shù)生成 CGImage 實(shí)例。
- 最后通過(guò)** CGImage** 實(shí)例生成修改了像素顏色的 UIImage 實(shí)例并返回。
/*
將圖像中出現(xiàn)次數(shù)最多的顏色修改為置頂?shù)念伾? */
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<UInt32>(repeating: 0, count: w * h)
// var bufferData = [UInt32](repeatElement(0, count: w*h))
let bufferData = UnsafeMutablePointer<UInt8>.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)
//傳進(jìn)來(lái)的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的內(nèi)存釋放,那么會(huì)導(dǎo)致新圖片賦值到imageView.image之后看不到圖片
// 應(yīng)該在創(chuàng)建 CGDataProvider 時(shí)的回調(diào)函數(shù)里面釋放
// bufferData.deinitialize(count: w*h)
// bufferData.deallocate()
return newImage
}
/*
獲取出現(xiàn)次數(shù)最多的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
}
// 先把圖片縮小 加快計(jì)算速度. 但越小結(jié)果誤差可能越大
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<UInt32>(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)
}
/*
將圖像中出現(xiàn)次數(shù)最多的顏色變?yōu)橥该魃? */
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 是一個(gè) Swift 數(shù)組,在從圖像中獲取像素?cái)?shù)據(jù)之后直接使用即可。
但是在這個(gè)例子里面 bufferData 是 UnsafeMutablePointer 類型。
原因是如果 bufferData 是Swift數(shù)組的話,在后面生成 CGDataProvider 實(shí)例并最終生成 UIImage 之后,放在控件上顯示不出來(lái)。
UnsafeMutablePointer<UInt32> 類型相當(dāng)于C語(yǔ)言的數(shù)組指針,即C語(yǔ)言的數(shù)組。若想使用該類型的變量的話需要自己創(chuàng)建并分配內(nèi)存和釋放。具體可以參考: https://blog.csdn.net/zkh90644/article/details/52819002
還有一點(diǎn)需要注意的是,如果使用 UnsafeMutablePointer 變量,就不需要在傳參的時(shí)候?qū)懮先〉刂贩?amp;bufferData)了,如果是普通的Swift數(shù)組的話則需要加上
【注意2】
這里我使用了 UInt8 數(shù)組來(lái)存儲(chǔ)像素(上面的例子里面是UInt32數(shù)組),
原因是為了方便像素?cái)?shù)據(jù)中顏色分量的重新賦值,而不用做各種按位與和或的運(yùn)算。
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)
//傳進(jìn)來(lái)的alpha是歸一化的值,所以這里需要除以255.0
bufferData.advanced(by: byteStart+3).pointee = UInt8(alpha * 255.0)
}
}
【注意3】
修改完顏色并根據(jù)像素?cái)?shù)據(jù)生成CGImage的時(shí)候也要指明bitmapInfo,
因?yàn)樽罱K的圖片可以是半透明的,即我們需要alpha通道生效。
所以此時(shí)應(yīng)該指明 CGImageAlphaInfo 為 .premultipliedLast 或者 .premultipliedFirst。
- 設(shè)置圖片透明度
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
}
// 調(diào)用draw方法之后,圖片的downMirror的,所以這里需要提前做一下反轉(zhuǎn)和平移變換
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
}
- 二分法壓縮圖片大?。?/strong>
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..<endIndex)
guard let testString = String(data: testData, encoding: .ascii) else {
return .unknown
}
if testString.hasPrefix("RIFF") && testString.hasSuffix("WEBP") {
return .webP
} else {
return .unknown
}
}
}
以上就是我最近所總結(jié)的圖片和Bitmap相關(guān)的知識(shí),這期間參考了很多朋友的博客,學(xué)到了很多也給了我很多的思路,非常感謝。如果以后學(xué)到了更多相關(guān)知識(shí)還會(huì)繼續(xù)更新。
參考:
https://blog.csdn.net/rpf2014/article/details/52598280
http://www.itdecent.cn/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
http://www.itdecent.cn/p/52e6fec1b418
https://blog.csdn.net/jeffasd/article/details/78142067