JPEG形式で繰り返し保存すると画質が劣化する都市伝説をまじめに検証

TL;DR
=====
- JPEG画像の圧縮・展開は「人間が知覚できない程度のズレ」を許容するアルゴル。
- 繰り返し保存したとき、確かに数値上は劣化しているようには見える。ただ実害はない（はず）。
- 正直、画像データが判読不可能なくらいまで壊れる、という説明はちょっと怪しい。


書いたのは誰？
=============
- 15年以上組み込み屋さんだったのに、今年頭に左遷。非常に悲しい。画像処理ちょとわかる。

はじめに
========
人類は、何故に **「JPEG形式で繰り返し保存すると画質が劣化するという都市伝説」** を信じるのか…

- [新規保存で劣化していくJPEG形式の画像の様子をとらえたムービー](https://gigazine.net/news/20090419_jpg_movie/) 


都市伝説を放置しておくのは、えんじにゃーんとして悲しい。ちゃんと検証してみよう。

なお、実は結論は既にほかの方が[繰り返しJPEG圧縮と画質
原画像の空間周波数性質の影響](https://www.jstage.jst.go.jp/article/photogrst1964/60/2/60_2_113/_pdf/-char/ja)という論文を出されている。

環境
====
![Ubuntu-23.04](https://img.shields.io/badge/Ubuntu-23.04-Green) ![OpenCV-4.8.0](https://img.shields.io/badge/OpenCV-4.8.0-Green) ![libjpeg-8c](https://img.shields.io/badge/libjpeg-8c-Green) 

テストコード
===========
```Makefile
a.out : main.cpp
        g++ main.cpp -o a.out \
                -I /usr/local/include/opencv4 \
                -lopencv_core \
                -lopencv_quality \
                -lopencv_imgcodecs
```

```c++
#include <iostream>
#include <opencv2/core.hpp>
#include <opencv2/imgcodecs.hpp>
#include <vector>
#include <opencv2/quality/qualityssim.hpp>

int main(void)
{
  cv::Mat origImg = cv::imread("test.jpg", cv::IMREAD_COLOR );
  cv::Mat img = origImg.clone();
  for(int i = 0 ; i < 20 ; i ++ )
  {
      std::vector<uchar> buf;
      cv::Mat privImg = img.clone();

      cv::imencode(".jpg", img, buf);
      img = cv::imdecode(buf, cv::IMREAD_COLOR );

      cv::Scalar retOrig = cv::quality::QualitySSIM::compute(img, origImg, cv::noArray());
      cv::Scalar retPriv = cv::quality::QualitySSIM::compute(img, privImg, cv::noArray());

      std::cout << "i = " << i;
      std::cout << " <ORIG>";
      std::cout << " SSIM=" << retOrig ;
      std::cout << " <PRIV>";
      std::cout << " SSIM=" << retPriv ;
      std::cout << std::endl;
  }
  return 0;
}
```

実験結果
========
SSIMは1.0であれば「全く同じ画像」で、0.0になるほどずれている画像になる。

```
kmtr@kmtr-VMware-Virtual-Platform:~/work/build4-main/jpegtest$ ./a.out
i = 0 <ORIG> SSIM=[0.995108, 0.997485, 0.996675, 0] <PRIV> SSIM=[0.995108, 0.997485, 0.996675, 0]
i = 1 <ORIG> SSIM=[0.994834, 0.997316, 0.996453, 0] <PRIV> SSIM=[0.999379, 0.999584, 0.999496, 0]
i = 2 <ORIG> SSIM=[0.994745, 0.997275, 0.996401, 0] <PRIV> SSIM=[0.999813, 0.999895, 0.999876, 0]
i = 3 <ORIG> SSIM=[0.994701, 0.997261, 0.996382, 0] <PRIV> SSIM=[0.999916, 0.999967, 0.999958, 0]
i = 4 <ORIG> SSIM=[0.994672, 0.997253, 0.99637, 0] <PRIV> SSIM=[0.99995, 0.999984, 0.999981, 0]
i = 5 <ORIG> SSIM=[0.994652, 0.997249, 0.996364, 0] <PRIV> SSIM=[0.999965, 0.99999, 0.999989, 0]
i = 6 <ORIG> SSIM=[0.994639, 0.997245, 0.99636, 0] <PRIV> SSIM=[0.999977, 0.999994, 0.999994, 0]
i = 7 <ORIG> SSIM=[0.994627, 0.997243, 0.996358, 0] <PRIV> SSIM=[0.999983, 0.999996, 0.999996, 0]
i = 8 <ORIG> SSIM=[0.994619, 0.997241, 0.996355, 0] <PRIV> SSIM=[0.999988, 0.999997, 0.999997, 0]
i = 9 <ORIG> SSIM=[0.994614, 0.997239, 0.996354, 0] <PRIV> SSIM=[0.99999, 0.999997, 0.999998, 0]
i = 10 <ORIG> SSIM=[0.99461, 0.997238, 0.996353, 0] <PRIV> SSIM=[0.999993, 0.999998, 0.999999, 0]
i = 11 <ORIG> SSIM=[0.994606, 0.997237, 0.996352, 0] <PRIV> SSIM=[0.999995, 0.999999, 0.999999, 0]
i = 12 <ORIG> SSIM=[0.994603, 0.997236, 0.996352, 0] <PRIV> SSIM=[0.999995, 0.999999, 0.999999, 0]
i = 13 <ORIG> SSIM=[0.9946, 0.997236, 0.996351, 0] <PRIV> SSIM=[0.999997, 0.999999, 1, 0]
i = 14 <ORIG> SSIM=[0.994598, 0.997235, 0.996351, 0] <PRIV> SSIM=[0.999997, 0.999999, 1, 0]
i = 15 <ORIG> SSIM=[0.994596, 0.997235, 0.996351, 0] <PRIV> SSIM=[0.999998, 0.999999, 1, 0]
i = 16 <ORIG> SSIM=[0.994595, 0.997235, 0.99635, 0] <PRIV> SSIM=[0.999999, 1, 1, 0]
i = 17 <ORIG> SSIM=[0.994594, 0.997235, 0.99635, 0] <PRIV> SSIM=[0.999999, 1, 1, 0]
i = 18 <ORIG> SSIM=[0.994595, 0.997235, 0.99635, 0] <PRIV> SSIM=[0.999999, 1, 1, 0]
i = 19 <ORIG> SSIM=[0.994594, 0.997235, 0.99635, 0] <PRIV> SSIM=[1, 1, 1, 0]
```


考察
====

本当に画像は劣化しているのか？
----------------------------
右側のPRIV（前回）との比較結果を見れば、ほぼほぼ誤差が収束するように見えるだろう。

左側のORIG（原画像）との比較結果だけを見ると、こういう結論も言える。

**「JPEG形式で繰り返し保存すると画質が劣化しているじゃないか！！」** 

だが、ちょっと待ってほしい。**SSIM=0.005程度の差分は人間が知覚できるのだろうか？**

それでは、実際のサンプル画像を見てみよう。卵の右側の半透明な白身部分（麩との境界）あたりなどに若干の差異が生じている。ただ、人間が知覚できない程度と言える。

元画像
------
![test.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89434/e213ed7c-76c0-c2ea-8a21-85f2550ea5b9.jpeg)

0回目画像
---------
![00.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89434/e429fcc6-2a9e-b880-26a0-0f137a07ec3d.jpeg)

例えば20回上書き保存をしたらこのような画像になる。

19回目画像
---------
![19.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89434/34991c1a-d54e-becf-c7f9-4876c47de228.jpeg)

そうなると、こういう反論も考えられる。

**実際に目に見えて数値違ってますよね？詭弁じゃないんですか？**

ごめんね... **不可逆圧縮/展開はそもそもその「詭弁」ベースでの圧縮方式** なんだ。

どういうことかというと、あるJPEGデータがあっても、それを展開するときにどんな計算処理をするのかにｙとて、展開結果の画像は異なってしまう、というのがある。例えば、「整数で計算」「浮動小数で計算」といった方法でさえも、最終的な出力画像は異なる。

その中で、人間が知覚できない程度の誤差は存在しても「無視」せざるを得ない。

私は外から取ってきたJPEG画像を手元で圧縮したら劣化した、どういうことだ？
--------------------------------------------------------------------
これは単純な話で、JPEGファイルに含まれていた各種情報を、次回の圧縮に活用できていなかったからと思われる。

今回のテストでは、まったく同じパラメータで何度も繰り返し圧縮をしている。つまり、量子化処理を繰り返せば最終的には同じ結果にしかならない。

ところが、**外部で作ったJPEG画像を、手元で圧縮するときと異なるパラメータになっている場合、前よりも悪い条件を使って圧縮する場合もある** 。

例えば、ダウンロードしたときは圧縮率95%だったのに、手元で展開した後に、85%で圧縮したら、そりゃまあ画像は劣化しますよね、と。


まとめ
======
- JPEG画像の圧縮・展開は「人間が知覚できない程度のズレ」を許容するアルゴル。
- 繰り返し保存したとき、確かに数値上は劣化しているようには見える。ただ実害はない（はず）。
- 正直、画像データが判読不可能なくらいまで壊れる、という説明はちょっと怪しい。