PNG「可逆圧縮を真似したいなんてバカな考えは止めるべきだな、ハハハハ」JPEG「出来らぁ！！」

この記事は[OpenCV Advent Calendar 2023](https://qiita.com/advent-calendar/2023/opencv)の11日目の記事です。
他の記事は目次にまとめられています。

■ 茶番
======
- JPEG「ヘンッ　サイズ小さくしたけりゃ、不可逆圧縮すりゃあ、だれだって圧縮できらあ!!」
- PNG「ファッ　ハハハ　たしかにそうだ！あんたもプロならわかるだろ　**画像を劣化無しに圧縮したいならば、JPEGでは不可能だと！** まあうちのマネをしようなんてバカな考えはよすんだなハハハハ」
- JPEG「‥‥‥‥出来らあっ！」
- PNG 「いまなんていった？」
- JPEG「**JPEG Encoder/Decoderでも、画像を可逆圧縮できる**っていったんだよ!!」

■ TL;DR
=======
- libjpeg-turbo 3.0から、可逆圧縮サポートが入ったよ！
- でも、まあOpenCV imgcodecsとしてはpngがあるから要らない・・・
- せっかくなんで、コードdiffとかはまとめて供養するよ！！

◯これを書いた人
---------------
元組み込み屋さん　⇒　元社内ニート　⇒　（強いて言うならば）Web系エンジニア？（ど素人未満、というか、何もしていない？）

■ libjpeg-turbo
===============
さて、OpenCVが利用するJPEG Encoder/Decoderだが、現在はlibjpegではなくlibjpeg-turboが推奨となっている。しかしながら、資金難でこちらも苦しんでいる…

https://gigazine.net/news/20230704-there-may-never-be-libjpeg-turbo-3-1/

そんなlibjpeg-turboであるが、 v3.0より、lossless jpeg supportが機能追加されている。

[wikipediaのlossless jpeg](https://ja.wikipedia.org/wiki/Lossless_JPEG)に詳しく記載がされている。

では、さっそく使っていこう！

◯IMWRITEのフラグを新規追加する
-----------------------------
まず、`imwrite()`を呼び出すときのparamsに今回のlossless jpegを実行する際のパラメータを追加する。

```diff_cpp
diff --git a/modules/imgcodecs/include/opencv2/imgcodecs.hpp b/modules/imgcodecs/include/opencv2/imgcodecs.hpp
index 89bd6e1c1b..09c56d0012 100644
--- a/modules/imgcodecs/include/opencv2/imgcodecs.hpp
+++ b/modules/imgcodecs/include/opencv2/imgcodecs.hpp
@@ -91,6 +91,8 @@ enum ImwriteFlags {
        IMWRITE_JPEG_LUMA_QUALITY   = 5,  //!< Separate luma quality level, 0 - 100, default is -1 - don't use.
        IMWRITE_JPEG_CHROMA_QUALITY = 6,  //!< Separate chroma quality level, 0 - 100, default is -1 - don't use.
        IMWRITE_JPEG_SAMPLING_FACTOR = 7, //!< For JPEG, set sampling factor. See cv::ImwriteJPEGSamplingFactorParams.
+       IMWRITE_JPEG_LOSSLESS_PREDICTOR_SELECTION = 8, //!< For JPEG, set prediction method. See cv::ImwriteJPEGLosslessPredictorParams, default is IMWRITE_JPEG_LOSSY, required libjpeg-turbo 3.0+..
+       IMWRITE_JPEG_LOSSLESS_POINT_TRANSFORM = 9, //!< For JPEG(Lossless), set point transform, 0 - 7, default is 0(fully lossless), required libjpeg-turbo 3.0+.
        IMWRITE_PNG_COMPRESSION     = 16, //!< For PNG, it can be the compression level from 0 to 9. A higher value means a smaller size and longer compression time. If specified, strategy is changed to IMWRITE_PNG_STRATEGY_DEFAULT (Z_DEFAULT_STRATEGY). Default value is 1 (best speed setting).
        IMWRITE_PNG_STRATEGY        = 17, //!< One of cv::ImwritePNGFlags, default is IMWRITE_PNG_STRATEGY_RLE.
        IMWRITE_PNG_BILEVEL         = 18, //!< Binary level PNG, 0 or 1, default is 0.
@@ -119,6 +121,34 @@ enum ImwriteJPEGSamplingFactorParams {
        IMWRITE_JPEG_SAMPLING_FACTOR_444 = 0x111111  //!< 1x1,1x1,1x1(No subsampling)
      };
```

`IMWRITE_JPEG_SAMPLING_FACTOR=7`まで使っているので、今度は、8と9を割り当てる。
- IMWRITE_JPEG_LOSSLESS_PREDICTOR_SELECTION: Lossless JPEGでデータサイズを削減するためのプレフィルター（前処理）でどんなフィルターをかけるのかを選択（後述）
- IMWRITE_JPEG_LOSSLESS_POINT_TRANSFORM: Losslessに変換する前に、何ビット情報をtruncateするか、気にせず基本的に0でいいはず

◯ImwriteJPEGLosslessPredictorParams
------------------------------------
https://en.wikipedia.org/wiki/Lossless_JPEG にも記載があるが、lossless jpegの場合、前処理することでデータ量を削減している。

以後の説明ででる A,B,Cは、Xの予想をする際の近傍サンプルである。

とはいえ、例えばこのデータはAになりそうだ！とかBが都合がよさそうだ！とか予想するのは難しいので、`A`にして横方向の連続に強くしておくのが無難かなーと思います。

| . |   |   |   |
|---|---|---|---|
| . | C | B | . |
| . | A | X | . |
| . | . | . | . |

|enum                                   | mean                    |
|---------------------------------------|-------------------------|
|IMWRITE_JPEG_LOSSY                     | Lossy Jpeg Mode(Default)|
|IMWRITE_JPEG_LOSSLESS_PREDICTOR_A      | A                       |
|IMWRITE_JPEG_LOSSLESS_PREDICTOR_B      | B                       |
|IMWRITE_JPEG_LOSSLESS_PREDICTOR_C      | C                       |
|IMWRITE_JPEG_LOSSLESS_PREDICTOR_AB_C   | A + B - C               |
|IMWRITE_JPEG_LOSSLESS_PREDICTOR_AB_C_2 | A + (B - C) /2          |
|IMWRITE_JPEG_LOSSLESS_PREDICTOR_BA_C_2 | B + (A - C) /2          |
|IMWRITE_JPEG_LOSSLESS_PREDICTOR_AB_2   | (A + B) /2              |

◯TRANSFORM_POINT
-----------------

0にすると、fully losslessだけど、数字を大きくするとガツガツlossyになる。というか、7とか使い物になら無さそう。これは実行サンプルで。


## ◯パラメータ解析

jpeg encoderの中で、パラメータ解析している部分で、指定されたパラメータ拾う。

```diff_cpp
+
+            if( params[i] == IMWRITE_JPEG_LOSSLESS_PREDICTOR_SELECTION )
+            {
+                lossless_predictor_selection_value = params[i+1];
+            }
+            if( params[i] == IMWRITE_JPEG_LOSSLESS_POINT_TRANSFORM )
+            {
+                lossless_point_transform = params[i+1];
+            }
+            
```

## ◯jpeg_enable_lossless()にパラメータを通知する

libjpeg-turboのバージョンが、3.0以上であるかを確認し、必要に応じてCV_LOSSLESS_JPEG_SUPPORTEDの定義を立てる。

```diff_cpp
+#if defined(LIBJPEG_TURBO_VERSION_NUMBER) && LIBJPEG_TURBO_VERSION_NUMBER >= 3000000
+  #define CV_LOSSLESS_JPEG_SUPPORTED
+#endif
````


次に、jpeg_enable_lossless()に、先に求めたパラメータを詰め込んで呼び出す。なお、libjpegやlibjpeg-turbo v2.x を使っているとこの関数が存在しないので、ここは対応しておくこと。

```diff_cpp
@@ -744,6 +759,15 @@ bool JpegEncoder::write( const Mat& img, const std::vector<int>& params )
         }
 #endif // #if JPEG_LIB_VERSION >= 70

+        if( lossless_predictor_selection_value != IMWRITE_JPEG_LOSSY )
+        {
+#ifdef CV_LOSSLESS_JPEG_SUPPORTED
+            jpeg_enable_lossless( &cinfo, lossless_predictor_selection_value, lossless_point_transform );
+#else
+            CV_Error( Error::StsNotImplemented, "Lossless Jpeg is supported with libjpeg-turbo 3.0+");
+#endif
+        }
+
         jpeg_start_compress( &cinfo, TRUE );
```

必要な修正はこれだけだよ！簡単だね！！

■ 効果検証
=========

このあたりは、modules/imgcodecs/test/test_jpeg.cpp への追記になる。

◯可逆圧縮はできているのか？
--------------------------
YES。元々のpngデータから展開した画像とtransform_point=0の画像で、差分0であることを確認済み

```
EXPECT_EQ(0, cvtest::norm(img_jpg_lossless, img_raw, NORM_INF));
```

◯ファイルサイズはどうかな？
----------------------------
ファイルサイズはというと、善戦はしているものの、やはりPNGには勝てませんなあ……

元画像の半分くらいなので、まあまあ及第点ではありますが。

TPの数値を上げていくと、どんどん「のっぺり感」が増していく。ある意味、ノスタルジー！（でも画面全体が暗くなるのはやめてほしい）

| file           | size      |
|----------------|----------:|
|RAW(1920x1080x3)| 6,220,800 |
|オリジナルpng画像| 1,336,572 |
|TP=0            | 2,985,843 |
|TP=1            | 2,521,827 |
|TP=2            | 2,094,269 |
|TP=3            | 1,666,717 |
|TP=4            | 1,257923 |
|TP=5            | 933,821  |
|TP=6            | 700,422  |
|TP=7            | 565,345  |


全体画像はこんな感じ。このうち、木の幹部分に注目。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89434/53fd6e23-3171-08a9-6447-6fff2c142db5.png)


TP=0
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89434/ed923ddc-3c9b-1eaa-bc8c-2e4b1079992c.png)

TP=4
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89434/4396b1dd-ab2d-7633-4066-64c36a14da59.png)

TP=5
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89434/af375c3d-1133-4157-c10a-00379235f298.png)

TP=6
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89434/036b76b0-5371-a386-0717-f15c2c515201.png)

TP=7　もはやPC9801で見ているようだ…
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89434/76257aa0-e086-df28-ff63-a8551ef0affe.png)


■茶番２
=======
- お客「このlossless jpegってどんな画像データなの？」
- JPEG「扱いにくいよ」
- お客「え」
- JPEG「GIFとJPEGしかない頃は期待されていた画像フォーマットだったんだけどね…　」
- お客「えええ」
- JPEG「**libjpeg-turboのv3にならないと使えない...** 。標準機能ではどれも表示もできない。現時点で、わざわざJPEGの可逆圧縮を積極的にサポートする意味があまりない。ここまでパッチ書いておいて、プルリクエストもせずにお蔵入りだな、ハハハハ（大号泣）」
- PNG・お客「「ええええ・・・」」

■まとめ
=======
- libjpeg-turbo 3.0から、可逆圧縮サポートが入ったよ！
- でも、まあOpenCV imgcodecsとしてはpngがあるから要らない・・・
- せっかくなんで、コードdiffとかはまとめて供養するよ！！




■ 修正全体
=========

<details><summary>全DIFF</summary>

```diff_cpp
diff --git a/modules/imgcodecs/include/opencv2/imgcodecs.hpp b/modules/imgcodecs/include/opencv2/imgcodecs.hpp
index 89bd6e1c1b..09c56d0012 100644
--- a/modules/imgcodecs/include/opencv2/imgcodecs.hpp
+++ b/modules/imgcodecs/include/opencv2/imgcodecs.hpp
@@ -91,6 +91,8 @@ enum ImwriteFlags {
        IMWRITE_JPEG_LUMA_QUALITY   = 5,  //!< Separate luma quality level, 0 - 100, default is -1 - don't use.
        IMWRITE_JPEG_CHROMA_QUALITY = 6,  //!< Separate chroma quality level, 0 - 100, default is -1 - don't use.
        IMWRITE_JPEG_SAMPLING_FACTOR = 7, //!< For JPEG, set sampling factor. See cv::ImwriteJPEGSamplingFactorParams.
+       IMWRITE_JPEG_LOSSLESS_PREDICTOR_SELECTION = 8, //!< For JPEG, set prediction method. See cv::ImwriteJPEGLosslessPredictorParams, default is IMWRITE_JPEG_LOSSY, required libjpeg-turbo 3.0+..
+       IMWRITE_JPEG_LOSSLESS_POINT_TRANSFORM = 9, //!< For JPEG(Lossless), set point transform, 0 - 7, default is 0(fully lossless), required libjpeg-turbo 3.0+.
        IMWRITE_PNG_COMPRESSION     = 16, //!< For PNG, it can be the compression level from 0 to 9. A higher value means a smaller size and longer compression time. If specified, strategy is changed to IMWRITE_PNG_STRATEGY_DEFAULT (Z_DEFAULT_STRATEGY). Default value is 1 (best speed setting).
        IMWRITE_PNG_STRATEGY        = 17, //!< One of cv::ImwritePNGFlags, default is IMWRITE_PNG_STRATEGY_RLE.
        IMWRITE_PNG_BILEVEL         = 18, //!< Binary level PNG, 0 or 1, default is 0.
@@ -119,6 +121,34 @@ enum ImwriteJPEGSamplingFactorParams {
        IMWRITE_JPEG_SAMPLING_FACTOR_444 = 0x111111  //!< 1x1,1x1,1x1(No subsampling)
      };

+//! Imwrite JPEG specific flags used to support Lossless JPEG.
+/** These flags will be modify the prediction pixels method.
+
+See https://en.wikipedia.org/wiki/Lossless_JPEG
+
+A,B,C are neighboring pixels samples to used for prediction for X pixel.
+
+|   |   |   |   |
+|---|---|---|---|
+| . | C | B | . |
+| . | A | X | . |
+| . | . | . | . |
+
+@note
+When encoding/decoding lossless jpeg, some features are disabled or limited.
+(e.g. Color conversion, DCT/IDCT, ...) Details are described at libjpeg.txt at libjpeg-turbo 3.0+.
+
+*/
+enum ImwriteJPEGLosslessPredictorParams {
+       IMWRITE_JPEG_LOSSY                     =  0, //!< Lossy Jpeg Mode(Default)
+       IMWRITE_JPEG_LOSSLESS_PREDICTOR_A      =  1, //!< A
+       IMWRITE_JPEG_LOSSLESS_PREDICTOR_B      =  2, //!< B
+       IMWRITE_JPEG_LOSSLESS_PREDICTOR_C      =  3, //!< C
+       IMWRITE_JPEG_LOSSLESS_PREDICTOR_AB_C   =  4, //!< A + B - C
+       IMWRITE_JPEG_LOSSLESS_PREDICTOR_AB_C_2 =  5, //!< A + (B - C) /2
+       IMWRITE_JPEG_LOSSLESS_PREDICTOR_BA_C_2 =  6, //!< B + (A - C) /2
+       IMWRITE_JPEG_LOSSLESS_PREDICTOR_AB_2   =  7, //!< (A + B) /2
+     };

 enum ImwriteEXRTypeFlags {
        /*IMWRITE_EXR_TYPE_UNIT = 0, //!< not supported */
diff --git a/modules/imgcodecs/src/grfmt_jpeg.cpp b/modules/imgcodecs/src/grfmt_jpeg.cpp
index 506cebdf49..e71425e327 100644
--- a/modules/imgcodecs/src/grfmt_jpeg.cpp
+++ b/modules/imgcodecs/src/grfmt_jpeg.cpp
@@ -88,6 +88,10 @@ extern "C" {
   #undef CV_MANUAL_JPEG_STD_HUFF_TABLES
 #endif

+#if defined(LIBJPEG_TURBO_VERSION_NUMBER) && LIBJPEG_TURBO_VERSION_NUMBER >= 3000000
+  #define CV_LOSSLESS_JPEG_SUPPORTED
+#endif
+
 namespace cv
 {

@@ -638,6 +642,8 @@ bool JpegEncoder::write( const Mat& img, const std::vector<int>& params )
         int rst_interval = 0;
         int luma_quality = -1;
         int chroma_quality = -1;
+        int lossless_predictor_selection_value = IMWRITE_JPEG_LOSSY;
+        int lossless_point_transform = -1;
         uint32_t sampling_factor = 0; // same as 0x221111

         for( size_t i = 0; i < params.size(); i += 2 )
@@ -707,6 +713,15 @@ bool JpegEncoder::write( const Mat& img, const std::vector<int>& params )
                     break;
                 }
             }
+
+            if( params[i] == IMWRITE_JPEG_LOSSLESS_PREDICTOR_SELECTION )
+            {
+                lossless_predictor_selection_value = params[i+1];
+            }
+            if( params[i] == IMWRITE_JPEG_LOSSLESS_POINT_TRANSFORM )
+            {
+                lossless_point_transform = params[i+1];
+            }
         }

         jpeg_set_defaults( &cinfo );
@@ -744,6 +759,15 @@ bool JpegEncoder::write( const Mat& img, const std::vector<int>& params )
         }
 #endif // #if JPEG_LIB_VERSION >= 70

+        if( lossless_predictor_selection_value != IMWRITE_JPEG_LOSSY )
+        {
+#ifdef CV_LOSSLESS_JPEG_SUPPORTED
+            jpeg_enable_lossless( &cinfo, lossless_predictor_selection_value, lossless_point_transform );
+#else
+            CV_Error( Error::StsNotImplemented, "Lossless Jpeg is supported with libjpeg-turbo 3.0+");
+#endif
+        }
+
         jpeg_start_compress( &cinfo, TRUE );

         if( channels > 1 )
diff --git a/modules/imgcodecs/test/test_jpeg.cpp b/modules/imgcodecs/test/test_jpeg.cpp
index b7932a0020..59144c1ecc 100644
--- a/modules/imgcodecs/test/test_jpeg.cpp
+++ b/modules/imgcodecs/test/test_jpeg.cpp
@@ -2,6 +2,7 @@
 // It is subject to the license terms in the LICENSE file found in the top-level directory
 // of this distribution and at http://opencv.org/license.html
 #include "test_precomp.hpp"
+#include "opencv2/core/utils/logger.hpp"

 namespace opencv_test { namespace {

@@ -270,6 +271,92 @@ TEST(Imgcodecs_Jpeg, encode_subsamplingfactor_usersetting_invalid)
     }
 }

+
+typedef testing::TestWithParam<tuple<int,ImwriteJPEGLosslessPredictorParams,int>> Imgcodecs_Jpeg_Lossless;
+
+#include <jpeglib.h>
+#if defined(LIBJPEG_TURBO_VERSION_NUMBER) && LIBJPEG_TURBO_VERSION_NUMBER >= 3000000
+  #define CV_LOSSLESS_JPEG_SUPPORTED
+#endif
+
+TEST_P(Imgcodecs_Jpeg_Lossless, readwrite)
+{
+    const int imread_flag         = get<0>(GetParam());
+    const int predictor_selection = get<1>(GetParam());
+    const int point_transform     = get<2>(GetParam());
+
+    vector<int> param;
+    param.push_back( IMWRITE_JPEG_LOSSLESS_PREDICTOR_SELECTION );
+    param.push_back( predictor_selection );
+    param.push_back( IMWRITE_JPEG_LOSSLESS_POINT_TRANSFORM );
+    param.push_back( point_transform );
+
+    cvtest::TS& ts = *cvtest::TS::ptr();
+    string input = string(ts.get_data_path()) + "../perf/1280x1024.png";
+    cv::Mat img_raw = cv::imread(input, imread_flag);
+    ASSERT_FALSE(img_raw.empty());
+
+    std::vector<uchar> buf_jpg_lossless;
+    cv::Mat img_jpg_lossless;
+
+#ifdef CV_LOSSLESS_JPEG_SUPPORTED
+    ASSERT_NO_THROW(cv::imencode(".jpg", img_raw, buf_jpg_lossless, param));
+    ASSERT_NO_THROW(cv::imdecode(buf_jpg_lossless, imread_flag, &img_jpg_lossless));
+#else
+    if( predictor_selection == IMWRITE_JPEG_LOSSY )
+    {
+        ASSERT_NO_THROW(cv::imencode(".jpg", img_raw, buf_jpg_lossless, param));
+        ASSERT_NO_THROW(cv::imdecode(buf_jpg_lossless, imread_flag, &img_jpg_lossless));
+    }
+    else
+    {
+        ASSERT_THROW(cv::imencode(".jpg", img_raw, buf_jpg_lossless, param), cv::Exception );
+        ASSERT_THROW(cv::imdecode(buf_jpg_lossless, imread_flag, &img_jpg_lossless), cv::Exception);
+    }
+#endif
+    ASSERT_FALSE(img_jpg_lossless.empty());
+
+    if(point_transform == 0) // Quality checking for only fully lossless
+    {
+        EXPECT_EQ(0, cvtest::norm(img_jpg_lossless, img_raw, NORM_INF));
+    }
+
+    if(predictor_selection != IMWRITE_JPEG_LOSSLESS_PREDICTOR_A )
+    {
+        string fname = cv::format("%c-%d.jpg",  (imread_flag==IMREAD_COLOR)?'C':'G', point_transform);
+        ASSERT_NO_THROW(cv::imwrite(fname, img_raw, param));
+    }
+
+    {
+        string msg = cv::format("ColorMode=%s, PredictorSelection=%d, PointTransfer=%d -> %lu byte",
+            (imread_flag==IMREAD_COLOR)?"COLOR":"GRAY",
+            predictor_selection,
+            point_transform,
+            buf_jpg_lossless.size()
+        );
+        CV_LOG_INFO(NULL, msg);
+    }
+}
+
+INSTANTIATE_TEST_CASE_P(Imgcodecs_Jpeg, Imgcodecs_Jpeg_Lossless,
+    testing::Combine(
+        testing::Values( // Color
+            IMREAD_COLOR,
+            IMREAD_GRAYSCALE
+        ),
+        testing::Values( // Predictor Selection
+            IMWRITE_JPEG_LOSSLESS_PREDICTOR_A,
+            IMWRITE_JPEG_LOSSLESS_PREDICTOR_B,
+            IMWRITE_JPEG_LOSSLESS_PREDICTOR_C,
+            IMWRITE_JPEG_LOSSLESS_PREDICTOR_AB_C,
+            IMWRITE_JPEG_LOSSLESS_PREDICTOR_AB_C_2,
+            IMWRITE_JPEG_LOSSLESS_PREDICTOR_BA_C_2,
+            IMWRITE_JPEG_LOSSLESS_PREDICTOR_AB_2
+        ),
+        testing::Range(0,8) // Point Transfer
+    )
+);
+
 #endif // HAVE_JPEG

 }} // namespace
```

</details>