如何處理iPhone或iPad導(dǎo)出照片的方向

2015-06-08 14:25:11來源:libfeihu作者:

使用過iPhone或者iPad的朋友在拍照時不知是否遇到過這樣的問題,將設(shè)備中的照片導(dǎo)出到Windows上時,經(jīng)常發(fā)現(xiàn)導(dǎo)出的照片方向會有問題,要么橫著,要么顛倒著,需要旋轉(zhuǎn)才適合觀看。而如果直接在這些設(shè)備上瀏覽時,照片會始終顯示正確的方向……

使用過iPhone或者iPad的朋友在拍照時不知是否遇到過這樣的問題,將設(shè)備中的照片導(dǎo)出到Windows上時,經(jīng)常發(fā)現(xiàn)導(dǎo)出的照片方向會有問題,要么橫著,要么顛倒著,需要旋轉(zhuǎn)才適合觀看。而如果直接在這些設(shè)備上瀏覽時,照片會始終顯示正確的方向,在Mac上也能正確顯示。最近在iOS的開發(fā)中也遇到了同樣的問題,將拍攝的照片上傳到服務(wù)器后,再由Windows端下載該照片,發(fā)現(xiàn)手機(jī)上完全正常的照片到了這里顯示的橫七豎八。同一張照片為什么在不同的設(shè)備上表現(xiàn)的不同?如何能夠避免這種情況?本文將和大家一一解開這些問題。

照片的存儲演變

一切都得從相機(jī)的發(fā)展開始說起。

膠片時代

一般相機(jī)拍攝出來的畫面都是長方形,在拍攝的那一瞬間,它會將取景器中的場景對應(yīng)的顏色值存到對應(yīng)的像素位置。相機(jī)本身并沒有任何方向的概念,只是使用者想要拍攝的場景在他期望的照片中顯示的方式與實際存在差異時,才有了方向一說。如下圖,對一個場景F進(jìn)行拍攝,相機(jī)的方向可能會有這樣四個常見的角度:

攝像頭取景

相機(jī)是“自私”的,由于相機(jī)僅反應(yīng)真實的場景,它不理解拍攝的內(nèi)容,因此照片都以相機(jī)的坐標(biāo)系保存,于是上面四種情形實際拍攝出來的照片會像這樣:

存儲情況

最初的卡片機(jī)時代,照片都會經(jīng)由底片洗出來。那時不存在照片的方向問題,因為不管我們以何種角度拍攝,最終洗出來的照片,它本身非常容易旋轉(zhuǎn),所以我們總可以通過簡單的旋轉(zhuǎn)來觀看照片或者保存照片。比如這張照片墻中的照片,你能否說哪些照片是橫著?哪些顛倒著?你甚至都無法判斷每張照片相機(jī)是以何種角度拍攝的,因為每張都已經(jīng)旋轉(zhuǎn)至適合觀看的角度。

照片墻

數(shù)碼時代

可是到了數(shù)碼時代,不再需要底片,照片需要被存成一個圖像文件。對于上面的拍攝角度,存儲方式并沒有變化,所有的場景仍然是以相機(jī)的坐標(biāo)系來保存。于是這些照片仍像上面一樣,原封不動的保存了下來:

存儲情況

雖然存儲方式不變,和卡機(jī)機(jī)時代的實體相片不同的是,由于電腦屏幕可沒洗出來的照片那么容易旋轉(zhuǎn),所以照片只能夠以它存儲于磁盤中的方向來展示。這便是為何照片傳到電腦上之后,會出現(xiàn)橫了,或者顛倒的情況。正因為這樣,我們只有利用工具來旋轉(zhuǎn)照片才能夠正常觀看。

方向傳感器

為了克服這一情況,讓照片可以真實的反應(yīng)人們拍攝時看到的場景,現(xiàn)在很多相機(jī)中就加入了方向傳感器,它能夠記錄下拍攝時相機(jī)的方向,并將這一信息保存在照片中。照片的存儲方式還是沒有任何改變,它仍然是以相機(jī)的坐標(biāo)系來保存,只是當(dāng)相機(jī)來瀏覽這些照片時,相機(jī)可以根據(jù)照片中的方向信息,結(jié)合此時相機(jī)的方向,對照片進(jìn)行旋轉(zhuǎn),從而轉(zhuǎn)到適合人們觀看的角度。

但是很遺憾,這一標(biāo)準(zhǔn)并沒有被廣泛的傳播開來,或者說始終如一的貫徹,這也導(dǎo)致了本文所討論的問題。

EXIF(Exchangeable Image File Format)

那么,方向信息到底是記錄在照片的什么位置?

了解圖像格式的朋友可能會知道,圖像一般都由兩大部分組成,一部分是數(shù)據(jù)本身,它記錄了每個像素的顏色值,另外一部分是文件頭,這里面記錄著形如圖像的寬度,高度等信息。我們所討論的方向信息便是被存儲于文件頭中。更為具體一些:EXIF中,維基百科上對其的解釋為:

可交換圖像文件格式常被簡稱為Exif(Exchangeable image file format),是專門為數(shù)碼相機(jī)的照片設(shè)定的,可以記錄數(shù)碼照片的屬性信息和拍攝數(shù)據(jù)… Exif可以附加于JPEG、TIFF、RIFF等文件之中

注意:PNG格式的圖像中不包含。

Orientation

在EXIF涵蓋的各種信息之中,其中有一個叫做Orientation (rotation)的標(biāo)簽,用于記錄圖像的方向,這便是相機(jī)寫入方向信息的最終位置。它總共定義了八個值:

Orientation的八個值

注意:對于上面的八種方向中,加了*的并不常見,因為它們代表的是鏡像方向,如果不做任何的處理,不管相機(jī)以任何角度拍攝,都無法出現(xiàn)鏡像的情況。

這個表格代表什么意義?我們來看第一行,值為1時,右邊兩列的值分別為:Row #0 isTop,Column #0 is Left side,其實很好理解,它表示照片的第一行位于頂端,而第一列位于左側(cè),那么這張照片自然就是以正常角度拍攝的。

對著前面的四種拍攝角度,由于相機(jī)都是以其自身的坐標(biāo)系來保存照片,因此每張照片對應(yīng)的第一行和第一列的位置始終如下:

第一行第一列

我們來看第二張照片,這張照片需要逆時針旋轉(zhuǎn)90度才能夠正常觀看。旋轉(zhuǎn)之后,它的第一行位于左側(cè),而第一列位于下側(cè)。如此一來,對比表格,它的Orientation值為8。所以說,這個Orientation值提供了想要正常觀看圖像時應(yīng)該旋轉(zhuǎn)的方式。

以同樣的方法,我們可以推斷出上面四種方式拍攝時,對應(yīng)EXIF中Orientation的值如下所示:

圖片的方向

由于相機(jī)加上了方向傳感器的緣故,可以非常容易的檢測出以上幾種拍攝角度,并將角度對應(yīng)的Orientation值保存至圖像中。查看圖像時,相機(jī)檢測到其EXIF中的Orientation信息,并將圖像旋轉(zhuǎn)相應(yīng)的角度顯示給用戶,這樣便達(dá)到了智能顯示的目的。

iPhone上的情況

作為智能手機(jī)的重要組成部分,形形色色的傳感器自然必不可少。在iOS的設(shè)備中也是包含了這樣的方向傳感器,它也采用了同樣的方式來保存照片的方向信息到EXIF中。但是它默認(rèn)的照片方向并不是豎著拿手機(jī)時的情況,而是橫向,即Home鍵在右側(cè),如下:

iPhone正常方向

如此一來,如果豎著拿手機(jī)拍攝時,就相當(dāng)于對手機(jī)順時針旋轉(zhuǎn)了90度,也即上面相機(jī)圖片中的最后一幅,那么它的Orientation值為6。

iPhone豎向

驗證EXIF

在經(jīng)過上面的分析之后,我們來看看實際情況如何。我們分別在Mac和Windows平臺上對前面的論述做一個驗證。

Mac平臺

可以將照片從iOS設(shè)備中導(dǎo)出到Mac系統(tǒng)上,(注意,不能夠使用iPhoto或者Photos來導(dǎo)入,因為這樣照片在導(dǎo)入之前會被自動調(diào)整好方向)在這里我們像Windows中一樣,將iPhone當(dāng)成移動硬盤,直接訪問其照片。在Mac上可以使用iTools這一神器。

然后用Mac上的預(yù)覽程序查看其EXIF屬性,通過預(yù)覽-工具-顯示檢查器打開對話框,即可查看到照片中關(guān)于方向的詳細(xì)信息。下面四張圖分別展示了上面四種方向下拍得照片的Orientation值:

  • Home鍵位于右側(cè)時,即相機(jī)的默認(rèn)方向,值為1。

Home鍵在右側(cè)


 

  • Home鍵位于上側(cè)時,值為8。

Home鍵在上側(cè)


 

  • Home鍵位于左側(cè)時,值為3。
Home鍵在左側(cè)
  • Home鍵位于下側(cè)時,即正常手持手機(jī)的方向,值為6。

Home鍵在下側(cè)


 

對照前面的分析,完全一致。而且照片顯示正常,說明在Mac上默認(rèn)的預(yù)覽程序會自動的處理EXIF中的Orientation信息。

再次提醒:照片存儲在手機(jī)中始終是以相機(jī)坐標(biāo)系保存的,只是瀏覽工作在讀取方向信息之后做了旋轉(zhuǎn)。

Windows平臺

前面提到過,被寫在圖像文件頭中的方向信息并沒有被全部支持,Windows的照片查看器便是其中之一,這也是Windows用戶最常使用的照片瀏覽工具。因為沒有讀取方向信息,照片被讀入之后,完全按照其存儲方式來顯示,這樣便出現(xiàn)了橫向,或者顛倒的情況。下面四張圖便分別是上一節(jié)中拍得的照片在Windows上的顯示效果,注意看方向。

Windows上的情況

開發(fā)時如何避免

既然不是所有的工具都支持方向?qū)傩,這其中甚至包含了具有最多用戶群體的Windows,那么我們在開發(fā)照片相關(guān)的應(yīng)用時,有沒有什么應(yīng)對之策?

當(dāng)然有!因為可以非常容易的得到照片的方向信息,那么只需要在保存之前將照片旋轉(zhuǎn)至正常觀看的方向即可,然后直接將最終具有正確方向的照片保存下來,搞定。

當(dāng)我們得到一個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的定義一一對應(yīng):

  • Up UIImageOrientationUp
  • Down UIImageOrientationDown
  • Left UIImageOrientationLeft
  • Right UIImageOrientationRight
  • UpMirror UIImageOrientationUpMirrored
  • DownMirror UIImageOrientationDownMirrored
  • LeftMirror UIImageOrientationLeftMirrored
  • RightMirror UIImageOrientationRightMirrored

那么我們便可以根據(jù)這一屬性對圖像進(jìn)行相應(yīng)的旋轉(zhuǎn),從而將圖像的原始數(shù)據(jù)旋轉(zhuǎn)至正確的方向,在瀏覽照片時無需方向信息便可正常瀏覽。

關(guān)于如何旋轉(zhuǎn)圖像,StackOverflow上給出了很好的答案,比如這個。我們簡單做一個介紹:

直觀的解決方案

首先,為UIImage創(chuàng)建一個category,其中包含fixOrientation方法:

UIImage+fixOrientation.h

1 @interface UIImage (fixOrientation)
2 
3 - (UIImage *)fixOrientation;
4 
5 @end

UIImage+fixOrientation.m

 1 @implementation UIImage (fixOrientation)
 2 
 3 - (UIImage *)fixOrientation {
 4 
 5     // No-op if the orientation is already correct
 6     if (self.imageOrientation == UIImageOrientationUp) return self;
 7 
 8     // We need to calculate the proper transformation to make the image upright.
 9     // We do it in 2 steps: Rotate if Left/Right/Down, and then flip if Mirrored.
10     CGAffineTransform transform = CGAffineTransformIdentity;
11 
12     switch (self.imageOrientation) {
13         case UIImageOrientationDown:
14         case UIImageOrientationDownMirrored:
15             transform = CGAffineTransformTranslate(transform, self.size.width, self.size.height);
16             transform = CGAffineTransformRotate(transform, M_PI);
17             break;
18 
19         case UIImageOrientationLeft:
20         case UIImageOrientationLeftMirrored:
21             transform = CGAffineTransformTranslate(transform, self.size.width, 0);
22             transform = CGAffineTransformRotate(transform, M_PI_2);
23             break;
24 
25         case UIImageOrientationRight:
26         case UIImageOrientationRightMirrored:
27             transform = CGAffineTransformTranslate(transform, 0, self.size.height);
28             transform = CGAffineTransformRotate(transform, -M_PI_2);
29             break;
30         case UIImageOrientationUp:
31         case UIImageOrientationUpMirrored:
32             break;
33     }
34 
35     switch (self.imageOrientation) {
36         case UIImageOrientationUpMirrored:
37         case UIImageOrientationDownMirrored:
38             transform = CGAffineTransformTranslate(transform, self.size.width, 0);
39             transform = CGAffineTransformScale(transform, -1, 1);
40             break;
41 
42         case UIImageOrientationLeftMirrored:
43         case UIImageOrientationRightMirrored:
44             transform = CGAffineTransformTranslate(transform, self.size.height, 0);
45             transform = CGAffineTransformScale(transform, -1, 1);
46             break;
47         case UIImageOrientationUp:
48         case UIImageOrientationDown:
49         case UIImageOrientationLeft:
50         case UIImageOrientationRight:
51             break;
52     }
53 
54     // Now we draw the underlying CGImage into a new context, applying the transform
55     // calculated above.
56     CGContextRef ctx = CGBitmapContextCreate(NULL, self.size.width, self.size.height,
57                                              CGImageGetBitsPerComponent(self.CGImage), 0,
58                                              CGImageGetColorSpace(self.CGImage),
59                                              CGImageGetBitmapInfo(self.CGImage));
60     CGContextConcatCTM(ctx, transform);
61     switch (self.imageOrientation) {
62         case UIImageOrientationLeft:
63         case UIImageOrientationLeftMirrored:
64         case UIImageOrientationRight:
65         case UIImageOrientationRightMirrored:
66             // Grr...
67             CGContextDrawImage(ctx, CGRectMake(0,0,self.size.height,self.size.width), self.CGImage);
68             break;
69 
70         default:
71             CGContextDrawImage(ctx, CGRectMake(0,0,self.size.width,self.size.height), self.CGImage);
72             break;
73     }
74 
75     // And now we just create a new UIImage from the drawing context
76     CGImageRef cgimg = CGBitmapContextCreateImage(ctx);
77     UIImage *img = [UIImage imageWithCGImage:cgimg];
78     CGContextRelease(ctx);
79     CGImageRelease(cgimg);
80     return img;
81 }
82 
83 @end

代碼有些長,不過卻非常直觀。這里面涉及到圖像矩陣變換的操作,理解起來可能稍稍有些困難,接下來,我會有另外一篇文章專門來介紹圖像變換。現(xiàn)在,記住下面兩點便能夠很好的幫助理解:

  1. 圖像的原點在左下角
  2. 矩陣變換時,后面的矩陣先作用,前面的矩陣后作用

以UIImageOrientationDown方向為例,UIImageOrientationDown,很明顯它翻轉(zhuǎn)了180度。那么對它的旋轉(zhuǎn)需要兩步,第一步是以左下方為原點旋轉(zhuǎn)180度,(此時順時針還是逆時針旋轉(zhuǎn)效果一樣)旋轉(zhuǎn)后上圖變?yōu)椋?img alt="旋轉(zhuǎn)180度后" src="/uploadfile/2015/0608/20150608022522397.png" /> 。用代碼表示為:

1 transform = CGAffineTransformRotate(transform, M_PI);

因為是以左下方為原點旋轉(zhuǎn)的,所以整幅圖被移到了第三象限。第二步需要將其平移至第一象限,向右上方進(jìn)行平移即可。x方向上移動距離為圖像的寬度,y方向上移動距離為圖像的高度,所以平移后圖像變?yōu)椋?img alt="平移后" src="/uploadfile/2015/0608/20150608022523476.png" />。代碼為:

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上的答案,沒那么直觀,但非常簡單:

1 - (UIImage *)normalizedImage {
2     if (self.imageOrientation == UIImageOrientationUp) return self; 
3 
4     UIGraphicsBeginImageContextWithOptions(self.size, NO, self.scale);
5     [self drawInRect:(CGRect){0, 0, self.size}];
6     UIImage *normalizedImage = UIGraphicsGetImageFromCurrentImageContext();
7     UIGraphicsEndImageContext();
8     return normalizedImage;
9 }

這里是利用了UIImage中的drawInRect方法,它會將圖像繪制到畫布上,并且已經(jīng)考慮好了圖像的方向,開發(fā)文檔這樣解釋:

-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.

結(jié)尾

關(guān)于照片方向的處理就介紹到這里,相信看完本文你已經(jīng)知悉為何以及如何處理這個問題。

關(guān)于EXIF,這里面包含了很多有趣的內(nèi)容,比如iPhone拍攝后,可以記錄當(dāng)時的GPS位置,這樣在查看照片的時候就可以很神奇的知道照片的拍攝地。如果感興趣可以去一探究竟。

另外,除去專門的照片瀏覽工具,所有的現(xiàn)代瀏覽器也天生具備查看圖片的功能。而且有很多瀏覽器也已經(jīng)支持EXIF中的Orientation,比如Firefox, Chrome, Safari。但同樣很可惜,IE并不支持(一直到IE9.0尚不支持)。也許和Win7設(shè)計時并沒有這些具有方向傳感器的手機(jī)有關(guān),我從網(wǎng)上了解到,在當(dāng)初2012年收集building Windows8意見時,就有人提到過這一問題,希望能夠考慮圖片的方向信息,微軟也給出了回應(yīng):

(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,如果有使用過的,希望可以幫我驗證一下是否微軟已經(jīng)修復(fù)這個問題。

(全文完)


 

關(guān)鍵詞:iPhoneiPad照片