在計(jì)算機(jī)上存儲(chǔ)文本很容易。我們以字母或字符作為一個(gè)基本單元,建立了一個(gè)從數(shù)字到字符編碼的簡(jiǎn)單映射來進(jìn)行編碼。圖形信息則不同,有很多不同的方式來表示圖像,每種方式的優(yōu)點(diǎn)和缺點(diǎn)也都各有不同。
文本是線性且一維的。撇開文字方向的問題,我們只需要知道序列中的下一個(gè)元素是什么就可以有效的存儲(chǔ)文字。
圖像則更復(fù)雜。首先,它們是二維的,所以我們需要考慮如何表示圖像中某個(gè)特定位置的值。然后,我們需要考慮具體的值應(yīng)該如何量化。另外,根據(jù)我們捕捉圖像的途徑,也會(huì)有不同的方式來編碼圖形數(shù)據(jù)。一般來說,最直觀的方式是將其存為位圖數(shù)據(jù),可如果你想處理一組幾何圖形,效率就會(huì)偏低。一個(gè)圓形可以只由三個(gè)值 (兩個(gè)坐標(biāo)值和半徑) 來表示,使用位圖會(huì)使文件更大,卻只能做粗略的近似。
以上所說的問題向我們展示了不同圖像格式之間的第一個(gè)區(qū)別:位圖與矢量圖像。不同于位圖把值存在陣列中,矢量格式存儲(chǔ)的是繪圖圖像的指令。在處理一些可以被歸納為幾何形狀的簡(jiǎn)單圖像時(shí),這樣做顯然更有效率;但面對(duì)照片數(shù)據(jù)時(shí)矢量?jī)?chǔ)存就會(huì)顯得乏力了。建筑師設(shè)計(jì)房屋更傾向于使用矢量的方式,因?yàn)槭噶扛袷讲⒉粌H僅局限于線條的繪制,也可以用漸變或圖案的填充作為展示,所以利用矢量方式完全可以生成房屋的擬真渲染圖。
用于填充的圖案單元?jiǎng)t更適合被儲(chǔ)存為一個(gè)位圖,在這種情況下,我們可能需要一個(gè)混合格式。一個(gè)非常普遍的混合格式的一個(gè)例子是 PostScript,(或者時(shí)下比較流行的衍生格式,PDF),它基本上是一個(gè)用于繪制圖像的描述語(yǔ)言。上述格式主要針對(duì)印刷業(yè),而 NeXT 和 Adobe 開發(fā)的 Display Postscript 則是進(jìn)行屏幕繪制的指令集。PostScript 能夠排布字母,甚至位圖,這使得它成為了一個(gè)非常靈活的格式。
矢量格式的一大優(yōu)點(diǎn)是縮放。矢量格式的圖像其實(shí)是一組繪圖指令,這些指令通常是獨(dú)立于尺寸的。如果你想擴(kuò)大一個(gè)圓形,只需在繪制前擴(kuò)大它的半徑就可以了。位圖則沒這么容易。最起碼,如果擴(kuò)大的比例不是二的倍數(shù),就會(huì)涉及到重繪圖像,并且各個(gè)元素都只是簡(jiǎn)單地增加尺寸,成為一個(gè)色塊。由于我們不知道這圖像是一個(gè)圓形,所以無法確保弧線的準(zhǔn)確描繪,效果看起來肯定不如按比例繪制的線條那樣好。也因此,在像素密度不同的設(shè)備中,矢量圖像作為圖形資源會(huì)非常有用。位圖的話,同樣的圖標(biāo),在視網(wǎng)膜屏幕之前的 iPhone 上看起來并沒有問題,在拉伸兩倍后的視網(wǎng)膜屏幕上看起來就會(huì)發(fā)虛。就好像僅適配了 iPhone 的 App 運(yùn)行在 iPad 的 2x 模式下就不再那么清晰了。
雖然 Xcode 6 已經(jīng)支持了 PDF 格式,但迄今仍不完善,只是在編譯時(shí)將其創(chuàng)建成了位圖圖像。最常見的矢量圖像格式為 SVG,在 iOS 中也有一個(gè)渲染 SVG 文件的庫(kù),SVGKit。
大部分圖像都是以位圖方式處理的,從這里開始,我們就將重點(diǎn)放在如何處理它們上。第一個(gè)問題,是如何表示兩個(gè)維度。所有的格式都以一系列連續(xù)的行作為單元,而每一行則水平地按順序存儲(chǔ)了每個(gè)像素。大多數(shù)格式會(huì)按照行的順序進(jìn)行存儲(chǔ),但是這并不絕對(duì),比如常見的交叉格式,就不嚴(yán)格按照行順序。其優(yōu)點(diǎn)是當(dāng)圖像被部分加載時(shí),可以更好的顯示預(yù)覽圖像。在互聯(lián)網(wǎng)初期,這是一個(gè)問題,隨著數(shù)據(jù)的傳輸速度提升,現(xiàn)在已經(jīng)不再被當(dāng)做重點(diǎn)。
表示位圖最簡(jiǎn)單的方法是將二進(jìn)制作為每個(gè)像素的值:一個(gè)像素只有開、關(guān)兩種狀態(tài),我們可以在一個(gè)字節(jié)中存儲(chǔ)八個(gè)像素,效率非常高。不過,由于每一位只有最多兩個(gè)值,我們只能儲(chǔ)存兩種顏色。考慮到現(xiàn)實(shí)中的顏色數(shù)以百萬計(jì),上述方法聽起來并不是很有用。不過有一種情況還是需要用到這樣的方法:遮罩。比如,圖像的遮罩可以被用于透明性,在 iOS 中,遮罩被應(yīng)用在 tab bar 的圖標(biāo)上 (即便實(shí)際圖標(biāo)不是單像素位圖)。
如果要添加更多的顏色,有兩個(gè)基本的選擇:使用一個(gè)查找表,或直接用真實(shí)的顏色值。GIF 圖像有一個(gè)顏色表 (或色彩面板),可以存儲(chǔ)最多 256 種顏色。存儲(chǔ)在位圖中的值是該查詢列表中的索引值,對(duì)應(yīng)著其相應(yīng)的顏色。所以,GIF 文件僅限于 256 色。對(duì)于簡(jiǎn)單的線條圖或純色圖,這是一種不錯(cuò)的解決方法。但對(duì)于照片來說,就會(huì)顯示的不夠真實(shí),照片需要更精細(xì)的顏色深度。進(jìn)一步的改進(jìn)是 PNG 文件,這種格式可以使用一個(gè)預(yù)置的色板或者獨(dú)立的通道,它們都支持可變的顏色深度。在一個(gè)通道中,每個(gè)像素的顏色分量 (紅,綠,藍(lán),即 RGB,有時(shí)添加透明度值,即RGBA) 是直接指定的。
GIF 和 PNG 對(duì)于具有大面積相同顏色的圖像是最好的選擇,因?yàn)樗鼈兪褂玫?(主要是基于游程長(zhǎng)度編碼的) 壓縮算法可以減少存儲(chǔ)需求。這種壓縮是無損的,這意味著圖像質(zhì)量不會(huì)被壓縮過程影響。
一個(gè)有損壓縮圖像格式的例子是 JPEG。創(chuàng)建 JPEG 圖像時(shí),通常會(huì)指定一個(gè)與圖像質(zhì)量相關(guān)的壓縮比值參數(shù),壓縮程度過高會(huì)導(dǎo)致圖像質(zhì)量惡化。JPEG 不適用于對(duì)比鮮明的圖像 (如線條圖),其壓縮方式對(duì)類似區(qū)域的圖像質(zhì)量損害會(huì)相對(duì)嚴(yán)重。如果某張截圖中包含了文本,且保存為 JPEG 格式,就可以清楚地看到:生成的圖像中字符周圍會(huì)出現(xiàn)雜散的像素點(diǎn)。在大部分照片中不存在這個(gè)問題,所以照片主要使用 JPEG 格式。
總結(jié):就放大縮小而言,矢量格式 (如 SVG) 是最好的。對(duì)比鮮明且顏色數(shù)量有限的線條圖最適合 GIF 或 PNG (其中 PNG 更為強(qiáng)大),而照片,則應(yīng)該使用 JPEG。當(dāng)然,這些都不是不可逾越的規(guī)則,不過通常而言,對(duì)一定的圖像質(zhì)量與圖像尺寸而言,遵守規(guī)則會(huì)得到最好的結(jié)果。
在 iOS 中有幾個(gè)類是用來處理位圖數(shù)據(jù)的:UIImage
(UIKit),CGImage
(Core Graphics) 和 CIImage
(Core Image)。在創(chuàng)建以上類的實(shí)例之前,由 NSData
持有實(shí)際的數(shù)據(jù)。獲得一個(gè) UIImage
最簡(jiǎn)單的方法是使用 imageWithContentsOfFile:
方法,或者根據(jù)其來源,也可以使用 imageWithCGImage:
,imageWithCIImage:
或者 imageWithData:
。有多個(gè)不同卻相似的類看起來有點(diǎn)多余,部分原因是因?yàn)樗鼈儊碜圆煌目蚣?,?duì)于圖像儲(chǔ)存的優(yōu)化方式也各有側(cè)重,通常情況下,不同類型之間是可以輕松轉(zhuǎn)換的。
為了從相機(jī)得到一個(gè)圖像,我們需要?jiǎng)?chuàng)建一個(gè) AVCaptureStillImageOutput
對(duì)象。然后,我們可以使用 captureStillImageAsynchronouslyFromConnection: completionHandler:
方法。它的 handler
是以一個(gè) CMSampleBufferRef
類型為參數(shù)的 block。我們可以利用 AVCaptureStillImageOutput
中的類方法 jpegStillImageNSDataRepresentation:
將其轉(zhuǎn)換為一個(gè) NSData
對(duì)象, 接著,我們使用 imageWithData:
(上文提到過) 來得到一個(gè) UIImage
。在這個(gè)過程中,有許多參數(shù)可以調(diào)整,例如曝光控制或聚焦設(shè)定,光補(bǔ)償,閃光燈,甚至 ISO 設(shè)置 (僅 iOS 8)。設(shè)置會(huì)被應(yīng)用到一個(gè) AVCaptureDevice
,這個(gè)對(duì)象代表著一個(gè)存在于設(shè)備上的相機(jī)。
簡(jiǎn)單的圖像處理方法是使用 UIKit 的 UIGraphicsBeginImageContext
方法。調(diào)用之后,你就可以在當(dāng)前圖形上下文中繪制一些內(nèi)容,當(dāng)然也包括圖像本身。在我自己的 App —— Stereogram 中,我使用這個(gè)方法把兩個(gè)方形的圖像拼接,并在圖片上方添加了一個(gè)區(qū)域,放置了兩個(gè)點(diǎn)用于強(qiáng)調(diào)。代碼如下:
-(UIImage*)composeStereogramLeft:(UIImage *)leftImage right:(UIImage *)rightImage
{
float w = leftImage.size.width;
float h = leftImage.size.height;
UIGraphicsBeginImageContext(CGSizeMake(w * 2.0, h + 32.0));
[leftImage drawAtPoint:CGPointMake(0.0, 32.0)];
[rightImage drawAtPoint:CGPointMake(w, 32.0)];
float leftCircleX = (w / 2.0) - 8.0;
float rightCircleX = leftCircleX + w;
float circleY = 8.0;
[[UIColor blackColor] setFill];
UIRectFill(CGRectMake(0.0, 0.0, w * 2.0, 32.0));
[[UIColor whiteColor] setFill];
CGRect leftRect = CGRectMake(leftCircleX, circleY, 16.0, 16.0);
CGRect rightRect = CGRectMake(rightCircleX, circleY, 16.0, 16.0);
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:leftRect];
[path appendPath:[UIBezierPath bezierPathWithOvalInRect:rightRect]];
[path fill];
UIImage *savedImg = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return savedImg;
}
在畫布上排布好圖像并添加了兩個(gè)實(shí)心圓后,我們就可以調(diào)用一個(gè)方法把之前繪制好的圖形環(huán)境轉(zhuǎn)換成一個(gè) UIImage
。該方法的輸出如下所示:
http://wiki.jikexueyuan.com/project/objc/images/21-11.jpg" alt="" />
這張圖片由兩臺(tái)位置稍有不同的相機(jī)拍攝的兩張照片,以及一條具有兩個(gè)中心白點(diǎn)的用來輔助觀察的黑色條帶合成而來。
相較而言,將實(shí)際的像素混合排布是一件麻煩的事情。在一張立體圖像中,我們有兩張相鄰的照片,我們需要使我們的眼睛 (有時(shí)可以靠斗眼) 看到 3D 效果。還有一種選擇是所謂的 紅藍(lán) 3D(Anaglyphs),你可以使用彩色 3D 眼鏡觀看的紅/綠圖像。(下面列出的函數(shù),實(shí)現(xiàn)了鏈接中的 Optimized Anaglyphs 方法。)
在以這種方式處理單獨(dú)的像素時(shí),我們要使用 CGBitmapContextCreate
方法創(chuàng)建一個(gè)位圖繪制環(huán)境,它包括一個(gè)顏色空間 (例如 RGB)。然后,我們可以遍歷位圖 (包括左邊和右邊的照片),并獲得在各個(gè)顏色通道值。例如,我們維持一張圖片中綠色和藍(lán)色的原值,然后將藍(lán)色和綠色的值按一定方式計(jì)算后賦值給另一張圖片的紅色值:
UInt8 *rightPtr = rightBitmap;
UInt8 *leftPtr = leftBitmap;
UInt8 r1, g1, b1;
UInt8 r2, g2, b2;
UInt8 ra, ga, ba;
for (NSUInteger idx = 0; idx < bitmapByteCount; idx += 4) {
r1 = rightPtr[0]; g1 = rightPtr[1]; b1 = rightPtr[2];
r2 = leftPtr[0]; g2 = leftPtr[1]; b2 = leftPtr[2];
// r1/g1/b1 右側(cè)圖像,用于計(jì)算合并值
// r2/g2/b2 左側(cè)圖像,用于被合并值賦值
// ra/ga/ba 合并后的像素
ra = 0.7 * g1 + 0.3 * b1;
ga = b2;
ba = b2;
leftPtr[0] = ra;
leftPtr[1] = ga;
leftPtr[2] = ba;
rightPtr += 4; // 指向下一個(gè)像素 (4字節(jié), 包括透明度 alpha 值)
leftPtr += 4;
}
CGImageRef composedImage = CGBitmapContextCreateImage(_leftContext);
UIImage *retval = [UIImage imageWithCGImage:composedImage];
CGImageRelease(composedImage);
return retval;
在這個(gè)方法中,我們可以訪問實(shí)際的像素的所有信息,做任何我們喜歡的事情。不過,最好先查詢 Core Image 中是否已經(jīng)有濾鏡可用,因?yàn)樗鼈兪褂酶菀?,并且在通常情況下,它們比一些單像素值的處理方式更優(yōu)。
用于存儲(chǔ)圖像信息的標(biāo)準(zhǔn)格式是 Exif(可交換圖像文件格式)。在照片中,通常會(huì)捕獲照相的日期和時(shí)間,快門速度和光圈,如果設(shè)備支持,還包括 GPS 坐標(biāo)。這是基于 TIFF(標(biāo)簽圖像文件格式) 的一種標(biāo)簽系統(tǒng)。雖然它有很多缺陷,可作為一個(gè)現(xiàn)行標(biāo)準(zhǔn),確實(shí)沒有更好的選擇。通常情況下,有些方式會(huì)設(shè)計(jì)的更好,但我們大家所使用的相機(jī)都不支持。
在 iOS 中,可以使用 CGImageSourceCopyPropertiesAtIndex
方法訪問 Exif 信息。這個(gè)方法會(huì)返回一個(gè)包含所有相關(guān)信息的字典。不過,不要過于依賴這些附加的信息。由于供應(yīng)商定義的擴(kuò)展約定 (這不是一個(gè)通用的標(biāo)準(zhǔn)) 錯(cuò)綜復(fù)雜,數(shù)據(jù)經(jīng)常被丟失或損壞,尤其是當(dāng)圖像經(jīng)過許多不同的應(yīng)用程序 (如圖像編輯器等) 處理過后。一般來說,當(dāng)圖像被上傳到網(wǎng)絡(luò)服務(wù)器上時(shí),這些信息也會(huì)被抹掉;這些信息中有一些屬于敏感信息,例如 GPS 數(shù)據(jù)。出于對(duì)隱私的保護(hù),這些信息常常會(huì)被刪除。顯然,NS??A (美國(guó)國(guó)家安全局) 就在收割它們 XKeyscore 程序中的 Exif 數(shù)據(jù)。
處理圖像在有些時(shí)候是一個(gè)相當(dāng)復(fù)雜的問題。圖像處理這個(gè)問題已經(jīng)存在了一段時(shí)間,所以針對(duì)不同方面,會(huì)存在許多不同的框架。有時(shí)候,你不得不深入底層的 C 函數(shù)調(diào)用,解決隨之而來的手動(dòng)內(nèi)存管理問題。再者,圖像的來源很多,每個(gè)來源都會(huì)需要處理特定且不同的邊界問題。不過,iOS 中最大的問題其實(shí)是內(nèi)存:隨著相機(jī)和屏幕分辨率越來越好,圖像的尺寸也開始變大。iPhone 5S 有一個(gè) 800 萬像素的攝像頭;如果每個(gè)像素被存儲(chǔ)在4個(gè)字節(jié) (分別用于三個(gè)顏色信道,加上一個(gè)不透明性),就會(huì)產(chǎn)生 32 兆的數(shù)據(jù)。如果需要添加幾個(gè)工作副本或圖像的預(yù)覽,我們會(huì)很快在處理??多張圖片或幻燈片時(shí)遇到麻煩。再加上文件系統(tǒng)的寫入也并不是非常快,所以很有必要進(jìn)行一些優(yōu)化,以確保你的 iOS 應(yīng)用程序運(yùn)行流暢。