為什么我的CV模型不好用?沒想到原因竟如此簡單……
計算機視覺模型表現(xiàn)不佳的原因有很多,比如架構(gòu)設(shè)計缺陷、數(shù)據(jù)集代表性不足、超參數(shù)選擇失誤等。但有一個很簡單的原因卻常常被人們忽略:圖像的方向。機器學習專家 Adam Geitgey 近日發(fā)布了一篇文章探討了這一簡單卻又讓很多人頭痛的問題,并分享了他為解決這一問題編寫的自動圖像旋轉(zhuǎn)程序。
我寫過很多有關(guān)計算機視覺和機器學習項目的內(nèi)容,比如目標識別系統(tǒng)和人臉識別項目。我有一個開源的 Python 人臉識別軟件庫,算得上是 GitHub 上非常受歡迎的十大機器學習庫之一。這也意味著我常常收到關(guān)于 Python 和計算機視覺方面的新人提問。
以我的經(jīng)驗,有一個技術(shù)問題比其它任何問題都更容易讓人受挫——倒不是復雜的理論問題或昂貴 GPU 的問題。人們基本上沒意識到,幾乎所有人都是以側(cè)向方式將圖像載入內(nèi)存的,而計算機在檢測側(cè)向圖像中的目標或人臉時的能力可沒那么出色。
數(shù)碼相機如何自動旋轉(zhuǎn)圖像
當你在拍攝照片時,相機會感知你向哪邊傾斜。當你在另一個程序中查看照片時,它們會以正確的方向顯示。
但棘手的問題在于, 你的相機實際上并沒有在保存到磁盤中的文件中旋轉(zhuǎn)圖像數(shù)據(jù)。因為數(shù)碼相機中的圖像傳感器是逐行讀取的,最終匯集成連續(xù)的像素信息流。這能讓相機更輕松地保存像素數(shù)據(jù),因為不管相機的姿勢如何,像素數(shù)據(jù)總是以同樣的順序保存的。
實際上,照片能否以正確的方向顯示完全取決于圖像查看器應用。相機在保存圖像數(shù)據(jù)的同時還會保存有關(guān)每張圖片的元數(shù)據(jù)——相機設(shè)置、位置數(shù)據(jù)以及理所應當?shù)南鄼C的旋轉(zhuǎn)角度。圖像查看器應當使用這種信息來正確地顯示圖像。
圖像元數(shù)據(jù)最常見的格式是 Exif(Exchangeable image file forma「可交換圖像文件格式」的縮寫)。Exif 格式的元數(shù)據(jù)放在相機保存的 jpeg 文件中。你不能直接從圖像本身讀到這種 Exif 數(shù)據(jù),但可以使用任何知道如何讀取這一數(shù)據(jù)的程序進行讀取。
下面是使用 Exiftool 讀取的上面的鵝照片的 Exif 元數(shù)據(jù):
注意 Orientation(方向)這個數(shù)據(jù)元素。它能指示圖像查看器程序,在屏幕上顯示圖像之前將圖順時針旋轉(zhuǎn) 90 度。如果程序忘記這么做,圖像就會側(cè)向顯示。
為什么這讓很多 Python 計算機視覺應用表現(xiàn)不佳?
Exif 元數(shù)據(jù)并非 jpeg 文件格式的原生部分。在 TIFF 文件格式使用了這種元數(shù)據(jù)之后,jpeg 文件格式才加入這種元數(shù)據(jù)。其保持了與老一代圖像查看器的后向兼容性,但這也意味著某些程序根本沒有費心去解析 Exif 數(shù)據(jù)。
numpy、scipy、TensorFlow、Keras 等大多數(shù)用于處理圖像數(shù)據(jù)的 Python 庫都將自己視為研究通用數(shù)據(jù)數(shù)組的人的科學工具。所以它們不在乎消費者層面的問題,比如「圖像自動旋轉(zhuǎn)」——即使現(xiàn)在的所有相機拍照需要這種操作。
這差不多意味著,你用任意 Python 庫加載圖像時,都會得到未經(jīng)旋轉(zhuǎn)的原始圖像數(shù)據(jù)。現(xiàn)在猜猜看,當你將側(cè)向的或倒向的圖像輸入人臉識別或目標檢測模型會怎樣?因為你提供了錯誤的數(shù)據(jù),檢測器會提示失敗。
你可能認為這個問題僅限于新手或?qū)W生寫的 Python 腳本,但事實并非如此。即使谷歌的旗艦級 Vision API 演示也沒能正確地處理 Exif 方向:
谷歌的 Vision API 演示無法旋轉(zhuǎn)標準的手機拍攝的縱向圖像。
盡管谷歌的視覺技術(shù)能成功地檢測出側(cè)向圖像中存在一些動物,但它僅提供了一個不具體的「Animal(動物)」標簽。這是因為模型檢測側(cè)向的鵝要比檢測正向的鵝要困難得多。如果在輸入之前先正確地旋轉(zhuǎn)一下,則谷歌 Vision API 會得到如下的結(jié)果:
當圖像方向正確時,谷歌的檢測結(jié)果要具體得多——不僅能正確給出「Goose(鵝)」標簽,而且置信度分數(shù)要高得多,這就好多了。
如果你能如本演示中的那樣看到圖像是側(cè)向的,那么這個問題要明顯得多。但問題就在于你一般看不到。如今計算機上的一般程序都會以正確旋轉(zhuǎn)后的形式顯示圖像,而不是按照它實際在磁盤上存儲的側(cè)向數(shù)據(jù)的形式。所以當你想了解你的模型不能起效的原因而查看圖像時,圖像查看器會以正確的方向顯示,讓你無從了解你的模型效果差的原因。
Mac 上的 Finder 總是顯示應用了 Exif 旋轉(zhuǎn)后的圖像,這樣就沒法看到文件中的圖像數(shù)據(jù)實際上是側(cè)向的。
這不可避免地導致人們在 GitHub 上報告問題,說他們使用的開源項目根本不行或模型不夠準確。但事情的本質(zhì)非常簡單——他們輸入了側(cè)向甚至顛倒的圖像!
解決這個問題
解決方案是,每當你用 Python 程序加載圖像時,都執(zhí)行一次 Exif 方向元數(shù)據(jù)檢查,并在有需要時進行旋轉(zhuǎn)。做起來很簡單,不過在網(wǎng)上很難找到能為所有方向正確執(zhí)行旋轉(zhuǎn)的示例代碼。
下面是為任意圖像應用正確的方向后再將其載入 numpy 數(shù)組的代碼:
- import PIL.Image
- import PIL.ImageOps
- import numpy as np
- def exif_transpose(img):
- if not img:
- return img
- exif_orientation_tag = 274
- # Check for EXIF data (only present on some files)
- if hasattr(img, "_getexif") and isinstance(img._getexif(), dict) and exif_orientation_tag in img._getexif():
- exif_data = img._getexif()
- orientation = exif_data[exif_orientation_tag]
- # Handle EXIF Orientation
- if orientation == 1:
- # Normal image - nothing to do!
- pass
- elif orientation == 2:
- # Mirrored left to right
- img = img.transpose(PIL.Image.FLIP_LEFT_RIGHT)
- elif orientation == 3:
- # Rotated 180 degrees
- img = img.rotate(180)
- elif orientation == 4:
- # Mirrored top to bottom
- img = img.rotate(180).transpose(PIL.Image.FLIP_LEFT_RIGHT)
- elif orientation == 5:
- # Mirrored along top-left diagonal
- img = img.rotate(-90, expand=True).transpose(PIL.Image.FLIP_LEFT_RIGHT)
- elif orientation == 6:
- # Rotated 90 degrees
- img = img.rotate(-90, expand=True)
- elif orientation == 7:
- # Mirrored along top-right diagonal
- img = img.rotate(90, expand=True).transpose(PIL.Image.FLIP_LEFT_RIGHT)
- elif orientation == 8:
- # Rotated 270 degrees
- img = img.rotate(90, expand=True)
- return img
- def load_image_file(file, mode='RGB'):
- # Load the image with PIL
- img = PIL.Image.open(file)
- if hasattr(PIL.ImageOps, 'exif_transpose'):
- # Very recent versions of PIL can do exit transpose internally
- img = PIL.ImageOps.exif_transpose(img)
- else:
- # Otherwise, do the exif transpose ourselves
- img = exif_transpose(img)
- img = img.convert(mode)
- return np.array(img)
之后,你可以將這個圖像數(shù)據(jù)數(shù)組傳遞給需要的所有標準 Python 機器學習庫,比如 Keras 和 TensorFlow。
因為這個問題很常見,所以我將其制作成了一個 pip 庫,名為 image_to_numpy,你可以這樣安裝它:
- pip3 install image_to_numpy
你可以在任何 Python 程序中使用它來實現(xiàn)正確的圖像加載,比如:
- import matplotlib.pyplot as plt
- import image_to_numpy# Load your image file
- img = image_to_numpy.load_image_file("my_file.jpg")# Show it on the screen (or whatever you want to do)
- plt.imshow(img)
- plt.show()