如何處理iOS中照片的方向

轉載鏈接http://www.cocoachina.com/ios/20150605/12021.html
作者blog地址http://feihu.me/

1.jpg
1.jpg

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

<span style="color: rgb(0, 176, 80);">目錄</span>

  • 照片的存儲演變

  • 膠片時代

  • 數碼時代

  • 方向傳感器

  • EXIF(Exchangeable Image File Format)

  • Orientation

  • iPhone上的情況

  • 驗證EXIF

  • Mac平臺

  • Windows平臺

  • 開發時如何避免

  • 直觀的解決方案

  • 第二種簡單的方法

  • 結尾

<span style="color: rgb(0, 176, 80);">照片的存儲演變</span>

一切都得從相機的發展開始說起。

膠片時代

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

blob.png
blob.png

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

blob.png
blob.png

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

blob.png
blob.png

數碼時代

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

blob.png
blob.png

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

方向傳感器

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

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

<span style="color: rgb(0, 176, 80);">EXIF(Exchangeable Image File Format)</span>

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

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

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

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

Orientation

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

blob.png
blob.png

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

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

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

blob.png
blob.png

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

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

blob.png
blob.png

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

iPhone上的情況

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

blob.png
blob.png

如此一來,如果豎著拿手機拍攝時,就相當于對手機順時針旋轉了90度(圖像的原點在左下角),也即上面相機圖片中的最后一幅,那么它的Orientation值為6。

blob.png
blob.png

<span style="color: rgb(0, 176, 80);">驗證EXIF</span>

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

Mac平臺

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

然后用Mac上的預覽程序查看其EXIF屬性,通過預覽-工具-顯示檢查器打開對話框,即可查看到照片中關于方向的詳細信息。下面四張圖分別展示了上面四種方向下拍得照片的Orientation值:(這里的方向逆時針,參考上面的F圖來)

  • Home鍵位于右側時,即相機的默認方向,值為1。
blob.png
blob.png
  • Home鍵位于上側時,值為8。
blob.png
blob.png
  • Home鍵位于左側時,值為3。
blob.png
blob.png
  • Home鍵位于下側時,即正常手持手機的方向,值為6。
blob.png
blob.png

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

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

Windows平臺

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

blob.png
blob.png
blob.png
blob.png

<span style="color: rgb(0, 176, 80);">開發時如何避免</span>

既然不是所有的工具都支持方向屬性,這其中甚至包含了具有最多用戶群體的Windows,那么我們在開發照片相關的應用時,有沒有什么應對之策?

當然有!因為可以非常容易的得到照片的方向信息,<span style="color: rgb(0, 176, 80);">那么只需要在保存之前將照片旋轉至正常觀看的方向即可,然后直接將最終具有正確方向的照片保存下來,搞定。</span>

當我們得到一個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的定義一一對應:

blob.png
blob.png

那么我們便可以根據這一屬性對圖像進行相應的旋轉,從而將圖像的原始數據旋轉至正確的方向,在瀏覽照片時無需方向信息便可正常瀏覽。

關于如何旋轉圖像,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

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

<span style="color: rgb(0, 176, 80);">
1.圖像的原點在左下角
2.矩陣變換時,后面的矩陣先作用,前面的矩陣后作用
</span>

以UIImageOrientationDown方向為例,
blob.png
blob.png

,很明顯它翻轉了180度。那么對它的旋轉需要兩步,第一步是以左下方為原點旋轉180度,(此時順時針還是逆時針旋轉效果一樣)旋轉后上圖變為:
blob.png
blob.png
。用代碼表示為:
1 transform = CGAffineTransformRotate(transform, M_PI);

因為是以左下方為原點旋轉的,所以整幅圖被移到了第三象限。第二步需要將其平移至第一象限,向右上方進行平移即可。x方向上移動距離為圖像的寬度,y方向上移動距離為圖像的高度,所以平移后圖像變為:
blob.png
blob.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上的答案,沒那么直觀,但非常簡單:

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

<span style="color: rgb(0, 176, 80);">結尾</span>

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

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

(全文完)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,117評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,860評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,128評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,291評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,025評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,421評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,477評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,642評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,177評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,970評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,157評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,717評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,410評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,821評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,053評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,896評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,157評論 2 375

推薦閱讀更多精彩內容