為什麼圖片反覆壓縮後會普遍會變綠而不是其他顏色?

很多帖子里的表情圖片都是偏綠的,因為被多次下載並使用過,但為什麼是綠色?
比如這樣:


業餘版概要:安卓的一個核心的部分的代碼,為了優化執行速度進行了魔改,結果寫錯了代碼。結果導致 JPG 圖片壓縮發綠、崩壞。與安卓上的應用無關,它們是受害者(

專業版概要:問題出在 Android 提供的壓縮圖片介面上,準確的說是一個 Android 里一個叫做 Skia 的庫上。而這個 bug 在 2016 年 4 月中旬被修復了,如果按照 Android 的發行來看,那就是從 Android 7 (Nougat) 開始才消除這個問題。
(不是百度的陰謀。(認真)

前言:剛才在社區里和 @StarBrilliant 等人一起研究,現在應該可以下一個精確的定論了。如他的答案所說,問題出在 RGB 色彩空間轉換到 YUV 的時候。但問題不僅僅是精度下降,最大的問題是,錯誤的舍入(向下取整)。另外,JDCT_IFAST 方法會導致圖片嚴重劣化:「格子狀崩壞」、灰塊、黑白塊、畫面粗糙,但是題目問的僅僅是變綠,就不在這上面浪費篇幅了。

網頁模擬 by @StarBrilliant :JPEGreen Simulator

歷史性的修復:Use libjpeg-turbo for YUV-&>RGB conversion in jpeg encoder · google/skia@c7d01d3 · GitHub

=================================
# 是誰的鍋?

百度貼吧是最多人批評的,而且……出事的客戶端僅僅是 Android 系統上的。

我後來注意到 QQ 也有這個問題,特別是上傳頭像。以前一直不知道為什麼有一些圖稍微有點綠,以為是打開了新世界的大門(x

後來做了一點微小的測試,注意到百度貼吧、QQ,都會用 Android 系統提供的介面:

Bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream);

看起來都很乾凈……難不成是系統的問題?

我自己做了一個我這輩子寫的第一個 Android 的小程序(我真不敢斗膽叫做 App),模仿一個正常的應用,反覆 JPEG 壓縮。發現還真是那麼回事。順便完善了一下,做成了「效果拔群的綠化器」。

源代碼已開放:terribleGreen/MainActivity.java at master · LionNatsu/terribleGreen · GitHub(開源許可證:Apache License Version 2.0,歡迎提供 PR)

現在就要說到 Android 系統到底為什麼出了這個問題了。Android 系統自起誕生以來就引入了名為 Skia 的圖像庫(Google 自家產品),用於處理圖像,其中包括把圖片壓縮成 JPEG(平時說的 JPG)。而 Skia 又是調用 libjpeg-turbo 來實現真正的壓縮過程的。為了達到更好的壓縮效果,JPEG 演算法本身,將通常屏幕上表示顏色的 RGB(紅綠藍)數值,轉換為 YUV 數值(亮度,藍色分量,紅色分量)。正常情況下這個演算法是輕微有損的。

但是 Skia 不走尋常路,將這個變換演算法的各個常數複製到自己的代碼里(當然是合法地),然後降低了精度,以達到更高的速度(專業準確地說,從 16 位定點數,降低到了 8 位定點數),這導致了更大的損傷。

最可怕的是……在進行這個變換運算的最後一步,需要除以 256,而代碼中,採用了右移操作代替除法以提高執行速度(看不懂可以跳過):

int y = ( CYR*r + CYG*g + CYB*b ) &>&> CSHIFT;
int u = ( CUR*r + CUG*g + CUB*b ) &>&> CSHIFT;
int v = ( CVR*r + CVG*g + CVB*b ) &>&> CSHIFT;
// C?? 是已經擴大到 2^CSHIFT 倍的矩陣參數(-0.5 ~ 0.5),CSHIFT = 8

這個操作並沒有什麼問題,數學意義就是除以 256。但是問題出在:

1、直接截斷了小數部分,等價於 trunc()。如果符號數是用補碼實現的。即全部往負數方向取整。如:1.2 → 1; 3.9 → 3;0.0 → 0;-5.1 → -6.

2、較冒險的符號數移位:根據規範的定義,對符號數(可正可負的數)使用移位的效果將由具體的編譯器明確定義決定(implementation-defined)。因為移位是一個符號無關的操作,對符號數移位將依賴於符號數的具體表現形式。而這個形式 C++ 沒有給出一個限定,由具體的編譯器自行決定,對於非「補碼」(2"s complement)的情況結果可能並不是所期待的那樣數值整除2的冪。這裡假設了編譯器都能「正確」理解為整除。

=================================
# YUV 值向負方向取整導致什麼?

複習一下 YUV 的定義:

  1. Y,亮度,0.0 ~ 1.0;
  2. U,或者叫做 Cb,藍色分量,-0.5 ~ 0.5;
  3. V,或者叫做 Cr,紅色分量,-0.5 ~ 0.5。

在 Skia 的代碼里,YUV 三個值均對應到 0~255 的範圍。
因為向下取整,所以誤差在 1 一個單位以內:0/256 到 1/256 也就是,YUV 三個值都變小 0.00% 到 0.39% 這個範圍。

看一下 U, V 這兩個決定顏色的值是如何變化的:

(圖片來自 Tonyle, Wikimedia Commons, File:YUV UV plane.svg)

顯然,YUV 值向負方向取整,結果是呼之欲出的:變暗,變綠。(這裡的變暗是 YUV 里的 Y 減小,並不完全準確對應人類視覺的明暗概念)

這個錯誤的舍入,使得:所有在 0 ~ 255 範圍內非整數的 YUV 值都受到影響。那麼某個像素被舍入到整數之後,下一次再壓縮 JPEG 應該會好一些吧?很不幸的是,隨之而來的大量其他有損操作(比如 DCT 變換之後濾去高頻)又會使得 YUV 值發生變化:如果發生變化,假設隨機產生關於 0 對稱的誤差,那麼實際上也有 50% 的機率使得這個數值 -1,因為只要比原來的值小,都會被向下捨去。

這使得,圖片隨著 Skia 缺陷的色彩空間變換演算法反覆壓縮,越來越綠。

=================================
## 假如我們是 Skia 開發者,如何修復這個問題?(閱讀本節需要 C/C++ 常識)

交回給 libjpeg-turbo 庫自己來做色彩空間變換。這也正是本文開頭提到的那個歷史性的修復具體做的:把原本 Skia 庫 YUV 轉換代碼全部刪掉了,把這個過程留給整個過程最底層的 libjpeg-turbo 庫自己來做,並且用默認的 JDCT_ISLOW 方法代替 JDCT_IFAST 方法,那麼自然就沒這個問題了。

註:libjpeg-turbo 是個運用極其廣泛的庫。可以說,基本上電腦上手機上能見到的 JPEG 壓縮的地方用的一般都是 libjpeg-turbo。(iOS 應該也是吧?我沒有蘋果設備抱歉……Adobe 公司的魔法可能是另一回事)

如果不刪除呢?自己搗鼓:
* 本節所提到的代碼以及示例圖片可以在這裡找到:GitHub - LionNatsu/greenError: Discover the reason how `terribleGreen`(my another repo.) works on Android.

首先我們要模擬一個 Skia 的 libjpeg-turbo 操作(略),然後,在把圖片遞交給 libjpeg-turbo 之前,把色彩空間像 Skia 一樣,做一個變換(矩陣數據完全與 Skia 相同)。

我們所要做的修復就是,把運算改成能夠對數字進行合理四捨五入的運算:

int R=i[0], G=i[1], B=i[2];

#if 1 // Shift or float-divide (shift in Skia)
int Y = (R*CYR + G*CYG + B*CYB) &>&> CSHIFT;
int U = (R*CUR + G*CUG + B*CUB) &>&> CSHIFT;
int V = (R*CVR + G*CVG + B*CVB) &>&> CSHIFT;

o[0] = Y;
o[1] = U + 128;
o[2] = V + 128;
#else
double Y = (R*CYR + G*CYG + B*CYB) / pow(2, CSHIFT);
double U = (R*CUR + G*CUG + B*CUB) / pow(2, CSHIFT);
double V = (R*CVR + G*CVG + B*CVB) / pow(2, CSHIFT);

o[0] = round(Y);
o[1] = round(U + 128);
o[2] = round(V + 128);
#endif

這裡我把原版操作和修正版操作都寫在一起了,把 #if 1 改成 #if 0 即可切換。(為什麼我要說這些= =)

示例:左邊為原版 Lena 醬,右邊均為壓縮質量設置為 80%,重複 30 次。

完全 Skia 原版效果(即 Android 的):8-bit 變換,移位除法,JDCT_IFAST 方法。

畫質嚴重劣化,色彩偏綠。

不辣眼睛修正效果:8-bit 變換,移位除法,JDCT_FLOAT 方法。

可以看到關閉 JDCT_IFAST 之後畫面細膩了。

繼續修復舍入漏洞的效果:8-bit 變換,正常舍入的除法JDCT_FLOAT 方法。

可以看到色彩偏綠的問題被正確四捨五入修正了。

回歸原版 libjpeg-turbo 的壓縮效果(現在的新版 Android):16-bit 變換,正常舍入的除法JDCT_FLOAT 方法。(其實原版是JDCT_ISLOW,但差別不大)

比起 8-bit,少了很多「色斑」,因為精度高了,色彩解析度更高,或者說顏色的層次更加細膩。

=======
番外
Q:為什麼不用全身版 Lena (http://www.lenna.org/full/l_hires.jpg) 做示例圖?
A:……

(二營長,你他娘的義大利炮呢?!)

=================================
來一個小的總結,給非專業的旁友們看:
圖片變綠是安卓系統一直以來的問題,直到 Android 7 才修復。原因是安卓系統內部的一個核心部件的代碼,為了優化手機上運行的速度——寫錯了 = =。

2016.8.26, 21:54 發布
2016.8.26, 22:32 修訂:修正表述錯誤,高亮
2016.8.26, 22:34 修訂:添加 S.B. 的網頁模擬工具地址
2016.8.26, 23:05 修訂:添加概要
2016.8.26, 23:56 修訂:同步示例代碼
2016.8.27, 00:38 修訂:調整令人困惑的表述
2016.8.27, 14:38 修訂:訂正錯字
2016.8.27, 23:29 修訂:明確闡述各修復步驟的變化
2016.8.27, 23:31 修訂:該死的我漏了句號
2016.8.30, 00:45 修訂:對符號數移位的定性從「未定義的行為」修正為「由具體實現決定」


這是 Android 圖像庫的一個 bug,
標準的 JPEG 在做色彩空間轉換的時候,會用至少 16 比特精度,
Android 圖快只用了 8 比特。
這個 bug 同時也影響了 Chrome 瀏覽器。
這個問題在 Android 7.0 和 Chrome 52 里修復了。
參見 Use libjpeg-turbo for YUV-&>RGB conversion in jpeg encoder · google/skia@c7d01d3 · GitHub
想體驗的可以試試我寫的 JPEGreen Simulator


實驗黨來了……

簡單實驗表明,JPEG壓縮並不會造成顏色向綠色偏移,實驗細節見下。論壇中確實有不少人說圖片在反覆下載上傳後顏色會變綠,這裡有一些例子,只能猜測是伺服器端對於圖片做了其他處理而造成的這一現象。如這裡 &>一張圖片反覆上傳下載90次,會發生什麼? 也有類似的發現。

之前確實看到過一個貼吧帖子是把一個街景的圖片反覆上傳下載最後變綠的。如果有朋友能找到原地址請告訴我,把那些圖片下載下來琢磨琢磨。自己再傳一遍實在是太寂寞了……

-------------------我是實驗細節的分割線---------------------
實驗很簡單,就是取一些圖片,對其反覆讀取 - JPEG低質量保存,然後觀察其最終形態。為了減少工具帶來的不確定性,分別使用了Python圖形庫PIL以及OpenCV的Python wrapper兩種工具進行實驗。
取了9張測試圖片,前6張是隨機拿了一些主色調不同、解析度不同的圖片,後3張是網上據說確實會變綠的圖片。JPEG保存時的質量嘗試了10,5,1三種質量,連續讀取保存100次。三種質量下都沒有觀察到顏色整體變綠。為了效果明顯,下面只貼一下質量為1時的最終結果。

其中每組第一張是原圖,cv後綴的是使用OpenCV的,pil後綴是使用PIL的。有些圖看不出差別是因為原圖解析度較高,我這裡打開大圖看還是效果挺明顯的。尤其注意的是第8張,低質量保存多次後變成黑白色了,這確實符合JPEG壓縮原理,色度已經完全被丟掉了。
另外實驗過程中還有一個有趣的發現。嘗試了把讀取-保存10次的圖片分別保存,結果發現在第3次保存之後,圖片大小再也沒有變過,肉眼也看不出差別了。懶得比較後續圖片的像素值了,不過估計是完全一樣的。我猜測這個演算法有一個特點是,像素滿足了某種條件之後(足夠低質量),保存它就不會進一步丟掉信息了,比如說量化那裡都已經整除了什麼的。


估計水印有關,google 一下可以找到基於綠色分量添加水印的文章:
Shodhganga@INFLIBNET: Digital image watermarking
具體可以看 [15_chapter 5.pdf]。
大體意思是綠色在 DCT 和其他「攻擊」下的魯棒性更好,所以基於此設計了添加和解碼水印的演算法。

懷疑是貼吧在轉存圖片時加入了水印,但是演算法設計不夠好,增加了綠色分量的比重,導致多次進行後圖片越來越綠。


會有一種古老的失傳的手藝,用祖傳的JPEG編碼器玄學調壓縮質量來壓出一張麻點最多的圖……

講道理在高票答主製作的在線版本里,壓綠很容易,壓黑也是常事,但是要做到下面幾個答主發的表情那樣滿圖麻點,還真的不容易(

所以感覺肯定還有其他版本的有毒的jpeg壓縮器在廣泛使用


——————先問是不是,再問為什麼。——————
首先,問題貌似沒有說是用JPEG壓縮的,不排除用其他壓縮格式的可能性。各位答主貌似一上來就先默認是JPEG壓縮了,當然說明JPEG在圖片界應用還是相當廣泛的。
觀察問題給出的圖片損傷情況,感覺不太像JPEG壓縮過度產生的損傷,眾所周知,JPEG壓縮最大的損傷機制在於量化,而量化後的圖片最易產生的是塊效應。
比如(我心愛的刀客特友情出鏡)
原始圖

JPEG壓縮圖:

會發現有明顯的塊效應產生。
然而,目前確實有研究表明,JPEG壓縮能夠使顏色發生變化,尤其是損傷嚴重的時候。
當然具體的損傷機制和現象可以寫論文闡述,這裡就不展開多講。
這裡想說的是:
目前沒有什麼研究表明反覆壓縮圖片能讓圖片偏向於綠色。
【當然我這樣說不太嚴謹,圖片損傷研究領域的論文整天都在談論各種神奇的評價演算法,倒是沒有發現有人提及過這一現象,也有可能是我閱讀的文獻還不夠多,如果有大神有相關資料,請不吝賜教,小女子拜謝!
然而這一現象確實發生了,而且普遍反映是在百度里發現的。
因此大膽推測會不會是圖片傳輸或者圖片格式轉換問題上出現問題。
比如上傳機制或者下載使得圖像在YUV空間上的數值整體偏移,又或者是浮點數什麼的強制轉換成整數。
好吧我的腦洞一到這方面就不起作用的。
謹以此答案拋磚引玉,希望炸出更多圖像處理方面的大神。
其實個人對JPEG壓縮在顏色損傷方面頗感興趣,有人如果有此方面心得請不要猶豫果斷私信交流之!!!不勝感激!!


RGB轉到YUV,色彩空間轉換,在壓縮過程中是很關鍵的一步.
YUV中,其中的UV分量是有符號的,可以認為是8比特有符號數據.
當然也可以說是無符號的,但是有128的偏移.其實兩種說法是一個意思.
因此在YUV空間中,#000000,其實是綠色.#008080,是黑色,#FF8080,是白色.這一點在另一回答的圖中,非常清晰.

但並不存在更為變綠的問題.
JPEG的有損壓縮,導致高頻部分的信息丟失,這會導致圖像越來越"平均",也就是在綠色分量變化明顯的地方(比如一個綠色的點),會在壓縮後,使得附近的像素,帶有被平均後的綠色.但是這個過程對於所有的顏色,是一致的.並不存在僅僅向綠色偏移的過程.

題主觀察到的僅僅是部分圖像而已.就像那隻貓的圖像中,其實也有明顯的灰色,和黃色色塊的.


不明白為什麼高票答案並沒有把事情講清楚就那麼多贊(見其評論)...難道因為滿足了知乎高贊三原則有圖有代碼有公式么=。=

然而從實驗黨的給出例子來看,確實是百度貼吧的鍋。
QQ群中的表情圖片的畫質是如何越傳越差的? - 知乎用戶的回答

摺疊區的兄弟們你們辛苦了...



歪個樓,多年後也許綠圖會成為一種復古情懷,它正好趕上了表情包文化大爆炸的時代,估計以後我們會特意把圖片弄綠來懷舊一下





所以說我的頭像


過多次的情況下,看太陽都是綠的,何況圖片。


強制重定向:zhihu.com/question/29345490


原諒色


盜圖盜綠了


在android上,可能不僅僅是編碼的問題,解碼和opencv,matlab解碼出來的都不一樣


百度貼吧的鍋


綠色比較護眼吧


變綠的中間過程
猜測是某種特定的壓縮演算法有bug導致的
(國外沒聽說過這種情況啊,雖然不用qq表情包,但國外也有推特之類的,並沒有發現微博上那種變綠的圖誒
只能認為是【特定演算法】導致的。。但究竟源頭在哪並不清除


推薦閱讀:

怎麼勸大四室友不要考計算機研?
特效化妝在計算機影視特效如此發達的今天是否是雞肋?
太空梭的機載計算機有什麼特點?
什麼樣的外部存儲技術有可能跟內存 (RAM) 讀取速度一樣快?
為什麼硬碟空閑的時候也要保持旋轉?

TAG:互聯網 | 計算機 | 圖像處理 | 圖片 | 計算機科學 |