使用過iPhone或者iPad的朋友在拍照時不知是否遇到過這樣的問題,將設備中的照片導出到Windows上時,經常發現導出的照片方向會有問題,要麼橫著,要麼顛倒著,需要旋轉才適合觀看。而如果直接在這些設備上浏覽時,照片會始終顯示正確的方向,在Mac上也能正確顯示。最近在iOS的開發中也遇到了同樣的問題,將拍攝的照片上傳到服務器後,再由Windows端下載該照片,發現手機上完全正常的照片到了這裡顯示的橫七豎八。同一張照片為什麼在不同的設備上表現的不同?如何能夠避免這種情況?本文將和大家一一解開這些問題。
目錄
照片的存儲演變
膠片時代
數碼時代
方向傳感器
EXIF(Exchangeable Image File Format)
Orientation
iPhone上的情況
驗證EXIF
Mac平台
Windows平台
開發時如何避免
直觀的解決方案
第二種簡單的方法
結尾
照片的存儲演變
一切都得從相機的發展開始說起。
膠片時代
一般相機拍攝出來的畫面都是長方形,在拍攝的那一瞬間,它會將取景器中的場景對應的顏色值存到對應的像素位置。相機本身並沒有任何方向的概念,只是使用者想要拍攝的場景在他期望的照片中顯示的方式與實際存在差異時,才有了方向一說。如下圖,對一個場景F進行拍攝,相機的方向可能會有這樣四個常見的角度:
相機是“自私”的,由於相機僅反應真實的場景,它不理解拍攝的內容,因此照片都以相機的坐標系保存,於是上面四種情形實際拍攝出來的照片會像這樣:
最初的卡片機時代,照片都會經由底片洗出來。那時不存在照片的方向問題,因為不管我們以何種角度拍攝,最終洗出來的照片,它本身非常容易旋轉,所以我們總可以通過簡單的旋轉來觀看照片或者保存照片。比如這張照片牆中的照片,你能否說哪些照片是橫著?哪些顛倒著?你甚至都無法判斷每張照片相機是以何種角度拍攝的,因為每張都已經旋轉至適合觀看的角度。
數碼時代
可是到了數碼時代,不再需要底片,照片需要被存成一個圖像文件。對於上面的拍攝角度,存儲方式並沒有變化,所有的場景仍然是以相機的坐標系來保存。於是這些照片仍像上面一樣,原封不動的保存了下來:
雖然存儲方式不變,和卡機機時代的實體相片不同的是,由於電腦屏幕可沒洗出來的照片那麼容易旋轉,所以照片只能夠以它存儲於磁盤中的方向來展示。這便是為何照片傳到電腦上之後,會出現橫了,或者顛倒的情況。正因為這樣,我們只有利用工具來旋轉照片才能夠正常觀看。
方向傳感器
為了克服這一情況,讓照片可以真實的反應人們拍攝時看到的場景,現在很多相機中就加入了方向傳感器,它能夠記錄下拍攝時相機的方向,並將這一信息保存在照片中。照片的存儲方式還是沒有任何改變,它仍然是以相機的坐標系來保存,只是當相機來浏覽這些照片時,相機可以根據照片中的方向信息,結合此時相機的方向,對照片進行旋轉,從而轉到適合人們觀看的角度。
但是很遺憾,這一標准並沒有被廣泛的傳播開來,或者說始終如一的貫徹,這也導致了本文所討論的問題。
EXIF(Exchangeable Image File Format)
那麼,方向信息到底是記錄在照片的什麼位置?
了解圖像格式的朋友可能會知道,圖像一般都由兩大部分組成,一部分是數據本身,它記錄了每個像素的顏色值,另外一部分是文件頭,這裡面記錄著形如圖像的寬度,高度等信息。我們所討論的方向信息便是被存儲於文件頭中。更為具體一些:EXIF中,維基百科上對其的解釋為:
可交換圖像文件格式常被簡稱為Exif(Exchangeable image file format),是專門為數碼相機的照片設定的,可以記錄數碼照片的屬性信息和拍攝數據… Exif可以附加於JPEG、TIFF、RIFF等文件之中
注意:PNG格式的圖像中不包含。
Orientation
在EXIF涵蓋的各種信息之中,其中有一個叫做Orientation (rotation)的標簽,用於記錄圖像的方向,這便是相機寫入方向信息的最終位置。它總共定義了八個值:
注意:對於上面的八種方向中,加了*的並不常見,因為它們代表的是鏡像方向,如果不做任何的處理,不管相機以任何角度拍攝,都無法出現鏡像的情況。
這個表格代表什麼意義?我們來看第一行,值為1時,右邊兩列的值分別為:Row #0 is Top,Column #0 is Left side,其實很好理解,它表示照片的第一行位於頂端,而第一列位於左側,那麼這張照片自然就是以正常角度拍攝的。
對著前面的四種拍攝角度,由於相機都是以其自身的坐標系來保存照片,因此每張照片對應的第一行和第一列的位置始終如下:
我們來看第二張照片,這張照片需要逆時針旋轉90度才能夠正常觀看。旋轉之後,它的第一行位於左側,而第一列位於下側。如此一來,對比表格,它的Orientation值為8。所以說,這個Orientation值提供了想要正常觀看圖像時應該旋轉的方式。
以同樣的方法,我們可以推斷出上面四種方式拍攝時,對應EXIF中Orientation的值如下所示:
由於相機加上了方向傳感器的緣故,可以非常容易的檢測出以上幾種拍攝角度,並將角度對應的Orientation值保存至圖像中。查看圖像時,相機檢測到其EXIF中的Orientation信息,並將圖像旋轉相應的角度顯示給用戶,這樣便達到了智能顯示的目的。
iPhone上的情況
作為智能手機的重要組成部分,形形色色的傳感器自然必不可少。在iOS的設備中也是包含了這樣的方向傳感器,它也采用了同樣的方式來保存照片的方向信息到EXIF中。但是它默認的照片方向並不是豎著拿手機時的情況,而是橫向,即Home鍵在右側,如下:
如此一來,如果豎著拿手機拍攝時,就相當於對手機順時針旋轉了90度,也即上面相機圖片中的最後一幅,那麼它的Orientation值為6。
驗證EXIF
在經過上面的分析之後,我們來看看實際情況如何。我們分別在Mac和Windows平台上對前面的論述做一個驗證。
Mac平台
可以將照片從iOS設備中導出到Mac系統上,(注意,不能夠使用iPhoto或者Photos來導入,因為這樣照片在導入之前會被自動調整好方向)在這裡我們像Windows中一樣,將iPhone當成移動硬盤,直接訪問其照片。在Mac上可以使用iTools這一神器。
然後用Mac上的預覽程序查看其EXIF屬性,通過預覽-工具-顯示檢查器打開對話框,即可查看到照片中關於方向的詳細信息。下面四張圖分別展示了上面四種方向下拍得照片的Orientation值:
Home鍵位於右側時,即相機的默認方向,值為1。
Home鍵位於上側時,值為8。
Home鍵位於左側時,值為3。
Home鍵位於下側時,即正常手持手機的方向,值為6。
對照前面的分析,完全一致。而且照片顯示正常,說明在Mac上默認的預覽程序會自動的處理EXIF中的Orientation信息。
再次提醒:照片存儲在手機中始終是以相機坐標系保存的,只是浏覽工作在讀取方向信息之後做了旋轉。
Windows平台
前面提到過,被寫在圖像文件頭中的方向信息並沒有被全部支持,Windows的照片查看器便是其中之一,這也是Windows用戶最常使用的照片浏覽工具。因為沒有讀取方向信息,照片被讀入之後,完全按照其存儲方式來顯示,這樣便出現了橫向,或者顛倒的情況。下面四張圖便分別是上一節中拍得的照片在Windows上的顯示效果,注意看方向。
開發時如何避免
既然不是所有的工具都支持方向屬性,這其中甚至包含了具有最多用戶群體的Windows,那麼我們在開發照片相關的應用時,有沒有什麼應對之策?
當然有!因為可以非常容易的得到照片的方向信息,那麼只需要在保存之前將照片旋轉至正常觀看的方向即可,然後直接將最終具有正確方向的照片保存下來,搞定。
當我們得到一個UIImage對象時,它有一個屬性叫:imageOrientation,這裡面便保存了方向信息:
Property The orientation of the receiver’s image. (read-only) Discussion Image orientation affects the way the image data is displayed when drawn. By default, images are displayed in the “up” orientation. If the image has associated metadata (such as EXIF information), however, this property contains the orientation indicated by that metadata. For a list of possible values for this property, see UIImageOrientation.
它剛好也可能為下面八種值,這些值可以和EXIF中Orientation的定義一一對應:
那麼我們便可以根據這一屬性對圖像進行相應的旋轉,從而將圖像的原始數據旋轉至正確的方向,在浏覽照片時無需方向信息便可正常浏覽。
關於如何旋轉圖像,StackOverflow上給出了很好的答案,比如這個。我們簡單做一個介紹:
直觀的解決方案
首先,為UIImage創建一個category,其中包含fixOrientation方法:
UIImage+fixOrientation.h
@interface UIImage (fixOrientation) - (UIImage *)fixOrientation; @end
UIImage+fixOrientation.m
@implementation UIImage (fixOrientation) - (UIImage *)fixOrientation { // No-op if the orientation is already correct if (self.imageOrientation == UIImageOrientationUp) return self; // We need to calculate the proper transformation to make the image upright. // We do it in 2 steps: Rotate if Left/Right/Down, and then flip if Mirrored. CGAffineTransform transform = CGAffineTransformIdentity; switch (self.imageOrientation) { case UIImageOrientationDown: case UIImageOrientationDownMirrored: transform = CGAffineTransformTranslate(transform, self.size.width, self.size.height); transform = CGAffineTransformRotate(transform, M_PI); break; case UIImageOrientationLeft: case UIImageOrientationLeftMirrored: transform = CGAffineTransformTranslate(transform, self.size.width, 0); transform = CGAffineTransformRotate(transform, M_PI_2); break; case UIImageOrientationRight: case UIImageOrientationRightMirrored: transform = CGAffineTransformTranslate(transform, 0, self.size.height); transform = CGAffineTransformRotate(transform, -M_PI_2); break; case UIImageOrientationUp: case UIImageOrientationUpMirrored: break; } switch (self.imageOrientation) { case UIImageOrientationUpMirrored: case UIImageOrientationDownMirrored: transform = CGAffineTransformTranslate(transform, self.size.width, 0); transform = CGAffineTransformScale(transform, -1, 1); break; case UIImageOrientationLeftMirrored: case UIImageOrientationRightMirrored: transform = CGAffineTransformTranslate(transform, self.size.height, 0); transform = CGAffineTransformScale(transform, -1, 1); break; case UIImageOrientationUp: case UIImageOrientationDown: case UIImageOrientationLeft: case UIImageOrientationRight: break; } // Now we draw the underlying CGImage into a new context, applying the transform // calculated above. CGContextRef ctx = CGBitmapContextCreate(NULL, self.size.width, self.size.height, CGImageGetBitsPerComponent(self.CGImage), 0, CGImageGetColorSpace(self.CGImage), CGImageGetBitmapInfo(self.CGImage)); CGContextConcatCTM(ctx, transform); switch (self.imageOrientation) { case UIImageOrientationLeft: case UIImageOrientationLeftMirrored: case UIImageOrientationRight: case UIImageOrientationRightMirrored: // Grr... CGContextDrawImage(ctx, CGRectMake(0,0,self.size.height,self.size.width), self.CGImage); break; default: CGContextDrawImage(ctx, CGRectMake(0,0,self.size.width,self.size.height), self.CGImage); break; } // And now we just create a new UIImage from the drawing context CGImageRef cgimg = CGBitmapContextCreateImage(ctx); UIImage *img = [UIImage imageWithCGImage:cgimg]; CGContextRelease(ctx); CGImageRelease(cgimg); return img; } @end
代碼有些長,不過卻非常直觀。這裡面涉及到圖像矩陣變換的操作,理解起來可能稍稍有些困難,接下來,我會有另外一篇文章專門來介紹圖像變換。現在,記住下面兩點便能夠很好的幫助理解:
圖像的原點在左下角
矩陣變換時,後面的矩陣先作用,前面的矩陣後作用
以UIImageOrientationDown方向為例,,很明顯它翻轉了180度。那麼對它的旋轉需要兩步,第一步是以左下方為原點旋轉180度,(此時順時針還是逆時針旋轉效果一樣)旋轉後上圖變為: 。用代碼表示為:
1 transform = CGAffineTransformRotate(transform, M_PI);
因為是以左下方為原點旋轉的,所以整幅圖被移到了第三象限。第二步需要將其平移至第一象限,向右上方進行平移即可。x方向上移動距離為圖像的寬度,y方向上移動距離為圖像的高度,所以平移後圖像變為:。代碼為:
1 transform = CGAffineTransformTranslate(transform, self.size.width, self.size.height);
再加上我們前面所說的第二點,矩陣變換時,後面的矩陣先作用,前面的矩陣後作用,那麼只需要將上面兩步顛倒即可:
1 transform = CGAffineTransformTranslate(transform, self.size.width, self.size.height); 2 transform = CGAffineTransformRotate(transform, M_PI);
其它的方向可以用完全一樣的方法來分析,這裡不再一一贅述。
第二種簡單的方法
第二種方法同樣也是StackOverflow上的答案,沒那麼直觀,但非常簡單:
- (UIImage *)normalizedImage { if (self.imageOrientation == UIImageOrientationUp) return self; UIGraphicsBeginImageContextWithOptions(self.size, NO, self.scale); [self drawInRect:(CGRect){0, 0, self.size}]; UIImage *normalizedImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return normalizedImage; }
這裡是利用了UIImage中的drawInRect方法,它會將圖像繪制到畫布上,並且已經考慮好了圖像的方向,開發文檔這樣解釋:
-drawInRect: Draws the entire image in the specified rectangle, scaling it as needed to fit. Discussion This method draws the entire image in the current graphics context, respecting the image’s orientation setting. In the default coordinate system, images are situated down and to the right of the origin of the specified rectangle. This method respects any transforms applied to the current graphics context, however.
結尾
關於照片方向的處理就介紹到這裡,相信看完本文你已經知悉為何以及如何處理這個問題。
關於EXIF,這裡面包含了很多有趣的內容,比如iPhone拍攝後,可以記錄當時的GPS位置,這樣在查看照片的時候就可以很神奇的知道照片的拍攝地。如果感興趣可以去一探究竟。
另外,除去專門的照片浏覽工具,所有的現代浏覽器也天生具備查看圖片的功能。而且有很多浏覽器也已經支持EXIF中的Orientation,比如Firefox, Chrome, Safari。但同樣很可惜,IE並不支持(一直到IE9.0尚不支持)。也許和Win7設計時並沒有這些具有方向傳感器的手機有關,我從網上了解到,在當初2012年收集building Windows8意見時,就有人提到過這一問題,希望能夠考慮圖片的方向信息,微軟也給出了回應:
(In Windows8)Explorer now respects EXIF orientation information for JPEG images. If your camera sets this value accurately, you will rarely need to correct orientation.
但我一直沒有用過Windows8,如果有使用過的,希望可以幫我驗證一下是否微軟已經修復這個問題。
(全文完)
feihu
2015.05.31 於 Shenzhen