4種在生產(chǎn)中擾亂計算機(jī)視覺模型的方法
簡介
本文不是關(guān)于模型的質(zhì)量。它甚至不涉及擴(kuò)展、負(fù)載平衡和其他DevOp。它是關(guān)于一個更普遍但有時會遺漏的事情:處理不可預(yù)測的用戶輸入。
在訓(xùn)練模型時,數(shù)據(jù)科學(xué)家?guī)缀蹩偸怯幸粋受控的數(shù)據(jù)環(huán)境。這意味著使用已經(jīng)準(zhǔn)備好的數(shù)據(jù)集,或者有時間和資源手動收集、合并、清理和檢查數(shù)據(jù)。通過準(zhǔn)確地執(zhí)行此操作,可以提高基于這些數(shù)據(jù)訓(xùn)練的模型的質(zhì)量。
假設(shè)模型足夠好,可以部署到生產(chǎn)中。在最好的情況下,仍然可以控制環(huán)境(經(jīng)典的服務(wù)器端部署)。但即便如此,用戶也會從各種設(shè)備和來源上傳圖像。在邊緣部署的情況下,還有一層復(fù)雜性:無法控制環(huán)境。
在這兩種情況下,模型都應(yīng)該立即響應(yīng),開發(fā)人員沒有時間手動檢查數(shù)據(jù)。因此,至關(guān)重要的是:
· 隨時準(zhǔn)備數(shù)據(jù)
· 盡可能接近訓(xùn)練時間進(jìn)行預(yù)處理
否則,實(shí)際生產(chǎn)模型的質(zhì)量可能會比預(yù)期的低很多。這種情況之所以發(fā)生,是因?yàn)槿藗冊跀?shù)據(jù)中引入了偏見,而這是模型所沒有預(yù)料到的。
真實(shí)案例
在接下來的內(nèi)容中,將介紹一家公司開發(fā)計算機(jī)視覺模型時遇到的4個問題。在將其傳遞給算法(主要是神經(jīng)網(wǎng)絡(luò)),以下部分都與圖像處理有關(guān):
· 存儲在EXIF中的方向
· 非標(biāo)準(zhǔn)顏色配置文件
· 圖像庫中的差異
· 調(diào)整算法
對于每一個項目,都會提供一個案例研究和一段代碼,在生產(chǎn)中解決這個問題。
存儲在EXIF中的方向
如今,人們使用移動相機(jī)拍照(約91%,而且這個數(shù)字還在增長)。
通常情況下,移動設(shè)備會以預(yù)先確定的固定方向存儲圖像,而不管拍照時相機(jī)的實(shí)際位置是什么;謴(fù)初始方向所需的旋轉(zhuǎn)角度作為元信息存儲在EXIF中。
在移動設(shè)備或現(xiàn)代桌面軟件上觀看此類圖像可能沒問題,因?yàn)樗鼈兛梢蕴幚磉@些EXIF信息。但是以編程方式加載圖像時,許多庫讀取原始像素數(shù)據(jù)并忽略元信息。這會導(dǎo)致錯誤的圖像方向。在下面的示例中,我使用了PIL,它是用于圖像處理的最流行的Python庫之一。
input_image = "data/cup.jpg"
image = PIL.Image.open(input_image)
np_image_initial = np.a(chǎn)rray(image)
向神經(jīng)網(wǎng)絡(luò)發(fā)送這樣的圖像可能會產(chǎn)生完全隨機(jī)的結(jié)果。要解決這個問題,還需要讀取EXIF信息,并相應(yīng)地旋轉(zhuǎn)/鏡像圖像。
# get image EXIF
image_exif = image.getexif()
# retrieve orientation byte
orientation = image_exif.get(0x0112)
print(orientation)
# 6
# get the corresponding transformation
method = {
2: PIL.Image.FLIP_LEFT_RIGHT,
3: PIL.Image.ROTATE_180,
4: PIL.Image.FLIP_TOP_BOTTOM,
5: PIL.Image.TRANSPOSE,
6: PIL.Image.ROTATE_270,
7: PIL.Image.TRANSVERSE,
8: PIL.Image.ROTATE_90,
}.get(orientation)
if method is not None:
# replace original orientation
image_exif[0x0112] = 1
# apply transformation
image = image.transpose(method)
np_image_correct = np.a(chǎn)rray(image)
PIL允許讀取EXIF元信息并對其進(jìn)行解析。它知道在哪里尋找方向。存儲必要信息的字節(jié)是0x0112,并且文檔說明了如何處理每個值,以及如何處理圖像來恢復(fù)初始方向。
使用EXIF代碼讀取并正確旋轉(zhuǎn)的杯子圖像
這個問題似乎已經(jīng)解決了,對嗎?嗯,還沒有。
讓我們試試另一張照片。這一次,我將使用現(xiàn)代的HEIF圖像格式,而不是舊的JPEG(請注意,需要一個特殊的PIL插件)。默認(rèn)情況下,iPhone以這種格式拍攝HDR圖片。
圖中顯示了站在門口的女孩的圖像及其元數(shù)據(jù)
一切看起來都一樣。讓我們試著用一種天真的方式從代碼中讀取它,而不看EXIF。
input_image = "data/person.heic"
image = PIL.Image.open(input_image)
np_image_initial = np.a(chǎn)rray(image)
圖像:
原始圖像方向正確!但EXIF建議將其逆時針旋轉(zhuǎn)90度!所以方位恢復(fù)代碼會失敗。
這是一個已知的問題,iPhone在HEIC拍攝時,由于某些不清楚的原因,iPhone存儲的原始像素是正確的,但在以這種格式拍照時,仍然在EXIF中保持傳感器方向。
因此,當(dāng)圖像在iPhone上以HEIC格式拍攝時,應(yīng)該修正算法并省略旋轉(zhuǎn)。
# get image EXIF
image_exif = image.getexif()
# get image format
image_format = image.format
# retrieve orientation byte
orientation = image_exif.get(0x0112)
print(orientation)
# 6
print(image_format)
# HEIF
# get the corresponding transformation
method = {
2: PIL.Image.FLIP_LEFT_RIGHT,
3: PIL.Image.ROTATE_180,
4: PIL.Image.FLIP_TOP_BOTTOM,
5: PIL.Image.TRANSPOSE,
6: PIL.Image.ROTATE_270,
7: PIL.Image.TRANSVERSE,
8: PIL.Image.ROTATE_90,
}.get(orientation)
if method is not None:
# replace original orientation
image_exif[0x0112] = 1
# apply transformation if not HEIF
if image_format != "HEIF":
image = image.transpmemoryviewe(method)
np_image_correct = np.a(chǎn)rray(image)
當(dāng)然,來自其他設(shè)備的圖像定向可能會有更多問題,而這段代碼可能并不詳盡。但它清楚地表明了一個人應(yīng)對用戶輸入的謹(jǐn)慎程度,以及即使沒有惡意意圖,用戶輸入的不可預(yù)測性。
非標(biāo)準(zhǔn)顏色配置文件
元信息隱藏了另一個挑戰(zhàn)。圖片可能會被拍攝并存儲在不同的顏色空間和顏色配置文件中。
有了顏色空間,它或多或少是清晰的。它定義了用于存儲每個像素的圖像顏色信息的通道。最常見的兩種顏色空間是RGB(紅-綠-藍(lán))和CMYK(青-品紅-黃-黑)。人們幾乎不會弄錯CMYK中的圖像,因?yàn)樗鼈冇胁煌瑪?shù)量的通道:RGB是3。
因此,由于輸入通道數(shù)量錯誤,將其發(fā)送到網(wǎng)絡(luò)會立即中斷。所以這種從CMYK到RGB的轉(zhuǎn)換很少被忘記。
顏色配置文件要復(fù)雜得多。這是輸入(攝像頭)或輸出(顯示)設(shè)備的特征。它說明了特定于設(shè)備的記錄或顯示顏色的方式。
RGB顏色空間有很多不同的配置文件:sRGB、Adobe RGB、Display P3等。每個配置文件定義了自己的方式,將原始RGB值映射到人眼感知的真實(shí)顏色。
這導(dǎo)致了一個事實(shí),即相同的原始RGB值可能意味著不同圖片中的不同顏色。要解決這個問題,需要仔細(xì)地將所有圖像的顏色配置文件轉(zhuǎn)換為一個選定的標(biāo)準(zhǔn)。
通常,它是一個sRGB配置文件,因?yàn)橛捎跉v史原因,它是所有網(wǎng)站的默認(rèn)顏色配置文件。
在iPhone上拍攝的照片通常有一個顯示P3顏色的配置文件。讓我們從代碼中讀取圖像,看看它在進(jìn)行顏色配置文件轉(zhuǎn)換和不進(jìn)行顏色配置文件轉(zhuǎn)換時是什么樣子。
input_image = "data/city.heic"
image_initial = PIL.Image.open(input_image)
# open default ICC profile which is sRGB
working_icc_profile = PIL.ImageCms.getOpenProfile(
"data/SRGB.icc"
)
# read ICC profile from the image
image_icc_profile = PIL.ImageCms.getOpenProfile(
io.BytesIO(image_initial.info["icc_profile"])
)
# convert an image to default ICC profile
image_converted = PIL.ImageCms.profileToProfile(
im=image_initial,
inputProfile=image_icc_profile,
outputProfile=working_icc_profile,
renderingIntent=PIL.ImageCms.INTENT_PERCEPTUAL,
outputMode="RGB"
)
image_initial_np = np.a(chǎn)rray(image_initial)
image_converted_np = np.a(chǎn)rray(image_converted)
可以看到,通過轉(zhuǎn)換讀取的圖像更生動,更接近原始圖像。根據(jù)圖片和顏色配置,這種差異可能更大或更小。
發(fā)送到神經(jīng)網(wǎng)絡(luò)的顏色(即像素值)可能會影響最終質(zhì)量并破壞預(yù)測。因此,恰當(dāng)?shù)嘏c他們合作至關(guān)重要。
圖像庫中的差異
正確讀取圖像是非常重要的。但與此同時,我們應(yīng)該盡快做到這一點(diǎn)。因?yàn)樵谠朴嬎銜r代,時間就是金錢。此外,客戶不想等待太久。
這里的最終解決方案是切換到C/C++。有時,在產(chǎn)生式推理環(huán)境中,它可能完全有意義。但如果你想留在Python生態(tài)系統(tǒng)中,有很多選擇。每個圖像庫都有自己的功能和速度。
直到現(xiàn)在,我只使用了PIL模塊。為了進(jìn)行比較,我選擇了另外兩個流行的庫:OpenCV和Scikit image。
讓我們看看每個庫讀取不同大小的JPEG圖像的速度。
def read_cv2(file):
image_cv2 = cv2.cvtColor(
cv2.imread(
file,
cv2.IMREAD_UNCHANGED
),
cv2.COLOR_BGR2RGB
)
return image_cv2
def read_pil(file):
image_pil = PIL.Image.open(file)
_ = np.a(chǎn)rray(image_pil)
return image_pil
def read_skimage(file):
image_skimage = skimage.io.imread(file)
return image_skimage
num_repeats = 10
# read 37 JPEG images of different sizes and measure time
times_read_cv2 = measure_read_times(read_cv2, num_repeats)
times_read_pil = measure_read_times(read_pil, num_repeats)
times_read_skimage = measure_read_times(read_skimage, num_repeats)
對于小圖像,幾乎沒有區(qū)別。但對于大型的,OpenCV的速度大約是PIL和Scikit圖像的1.5倍。根據(jù)圖像內(nèi)容和格式(JPEG、PNG等),這種差異可能從1.4x到2.0x不等。但總的來說,OpenCV要快得多。
網(wǎng)絡(luò)上還有其他可靠的基準(zhǔn)給出了大致相同的數(shù)字。對于圖像書寫來說,這種差異可能更為顯著:OpenCV的速度要快4到10倍。
另一個非常常見的操作是調(diào)整大小。人們幾乎總是在將圖像發(fā)送到神經(jīng)網(wǎng)絡(luò)之前調(diào)整圖像的大小。這就是OpenCV真正閃耀的地方。
times_resize_cv2 = measure_time(
f=lambda: cv2.resize(image_cv2, (1000, 1000), interpolation=cv2.INTER_LINEAR),
num=20,
)
times_resize_pil = measure_time(
f=lambda: image_pil.resize((1000, 1000), resample=PIL.Image.LINEAR),
num=20,
)
times_resize_skimage = measure_time(
f=lambda: skimage.transform.resize(image_skimage, (1000, 1000)),
num=20,
)
print(f"cv2: {np.mean(times_resize_cv2):.3f}s")
print(f"pil: {np.mean(times_resize_pil):.3f}s")
print(f"skimage: {np.mean(times_resize_skimage):.3f}s")
# cv2: 0.006s
# pil: 0.135s
# skimage: 4.531s
在這里,我拍攝了一張7360x4100的圖像,并將其調(diào)整為1000x1000。OpenCV比PIL快22倍,比Scikit image快755倍!
選擇正確的庫可以節(jié)省大量時間。需要注意的是,相同的調(diào)整大小算法可能會在不同的實(shí)現(xiàn)中產(chǎn)生不同的結(jié)果:
image_cv2_resized = cv2.resize(
image_cv2,
(1000, 1000),
interpolation=cv2.INTER_LINEAR
)
image_pil_resized = image_pil.resize(
(1000, 1000),
resample=PIL.Image.LINEAR
)
print(
"Original images are the same: ",
(image_cv2 == np.a(chǎn)rray(image_pil)).a(chǎn)ll()
)
print(
"Resized images are the same: ",
(image_cv2_resized == np.a(chǎn)rray(image_pil_resized)).a(chǎn)ll()
)
mae = np.a(chǎn)bs(image_cv2_resized.a(chǎn)stype(int) -
np.a(chǎn)rray(image_pil_resized, dtype=int)).mean()
print(
"Mean Absolute Difference between resized images: ",
f"{mae:.2f}"
)
# Original images are the same: True
# Resized images are the same: False
# Mean Absolute Difference between resized images: 5.37
在這里,人們可以注意到,我對OpenCV和PIL都使用線性插值進(jìn)行下采樣。原始圖像是相同的。但結(jié)果不同。差別非常顯著:每種像素顏色平均255個像素中有5個不同。
因此,如果在訓(xùn)練和推理過程中使用不同的庫來調(diào)整大小,可能會影響模型的質(zhì)量。所以我們應(yīng)該密切關(guān)注它。
調(diào)整算法
除了不同庫之間的速度差異外,即使在一個庫中,也有不同的調(diào)整大小算法。我們應(yīng)該選擇哪一個?至少,這取決于是否要減小圖像大。ㄏ虏蓸樱┗蛟黾訄D像大。ㄉ喜蓸樱。
有許多調(diào)整圖像大小的算法。它們產(chǎn)生的圖像質(zhì)量和速度不同。我只看這5個,它們足夠好、足夠快,并且在主流庫中得到支持。
下面提供的結(jié)果也與一些指南和示例一致。
image = cv2.cvtColor(
cv2.imread(
input_image,
cv2.IMREAD_UNCHANGED
),
cv2.COLOR_BGR2RGB
)
# define list of algorithms
algos = {
"AREA": cv2.INTER_AREA,
"NEAREST": cv2.INTER_NEAREST,
"LINEAR": cv2.INTER_LINEAR,
"CUBIC": cv2.INTER_CUBIC,
"LANCZOS": cv2.INTER_LANCZOS4,
}
# draw original image
plt.imshow(image[1500:2500, 5000:6000])
for algo in algos:
# downsample the original image with selected algorithm
image_resized = cv2.resize(image, (736, 410), interpolation=algos[algo])
# draw results
plt.imshow(image_resized[150:250, 500:600])
對于下采樣,“area”算法看起來最好。它產(chǎn)生的噪音和偽影最少。事實(shí)上,它是OpenCV進(jìn)行下采樣的首選。
現(xiàn)在讓我們進(jìn)行上采樣——將采樣后的圖像恢復(fù)到原始大小,看看在這種情況下哪種算法最有效。
# create downsampled image with preferred "area" algorithm
image_downsampled = cv2.resize(image, (736, 410), interpolation=cv2.INTER_AREA)
# draw original image
plt.imshow(image[1500:2500, 5000:6000])
for algo in algos:
# upsample the downsampled image back to the original size
# with the selected resizing algorithm
image_resized = cv2.resize(
image_downsampled,
(7360, 4100),
interpolation=algos[algo]
)
# draw results
plt.imshow(image_resized[1500:2500, 5000:6000])
對于上采樣,算法會產(chǎn)生更一致的結(jié)果。盡管如此,“cubic”插值看起來模糊程度最低,最接近原始(“l(fā)anczos”提供了類似的結(jié)果,但速度要慢得多)。
所以這里的最終結(jié)論是使用“area”插值進(jìn)行下采樣,使用“cubic”算法進(jìn)行上采樣。
請注意,正確的調(diào)整算法選擇在訓(xùn)練期間也很重要,因?yàn)樗梢蕴岣哒w圖像質(zhì)量。更重要的是,訓(xùn)練和推理階段的調(diào)整算法應(yīng)該是相同的,否則模型可能會出問題。
結(jié)論
這篇文章,描述了在計算機(jī)視覺領(lǐng)域工作多年期間多次遇到的真實(shí)案例和問題。如果處理不當(dāng),它們中的每一個都可能會顯著降低生產(chǎn)中的模型質(zhì)量。
感謝閱讀!
原文標(biāo)題 : 4種在生產(chǎn)中擾亂計算機(jī)視覺模型的方法
請輸入評論內(nèi)容...
請輸入評論/評論長度6~500個字
最新活動更多
-
即日-11.13立即報名>>> 【在線會議】多物理場仿真助跑新能源汽車
-
11月28日立即報名>>> 2024工程師系列—工業(yè)電子技術(shù)在線會議
-
12月19日立即報名>> 【線下會議】OFweek 2024(第九屆)物聯(lián)網(wǎng)產(chǎn)業(yè)大會
-
即日-12.26火熱報名中>> OFweek2024中國智造CIO在線峰會
-
即日-2025.8.1立即下載>> 《2024智能制造產(chǎn)業(yè)高端化、智能化、綠色化發(fā)展藍(lán)皮書》
-
精彩回顧立即查看>> 【限時免費(fèi)下載】TE暖通空調(diào)系統(tǒng)高效可靠的組件解決方案
推薦專題
- 高級軟件工程師 廣東省/深圳市
- 自動化高級工程師 廣東省/深圳市
- 光器件研發(fā)工程師 福建省/福州市
- 銷售總監(jiān)(光器件) 北京市/海淀區(qū)
- 激光器高級銷售經(jīng)理 上海市/虹口區(qū)
- 光器件物理工程師 北京市/海淀區(qū)
- 激光研發(fā)工程師 北京市/昌平區(qū)
- 技術(shù)專家 廣東省/江門市
- 封裝工程師 北京市/海淀區(qū)
- 結(jié)構(gòu)工程師 廣東省/深圳市