LoginSignup
13
5

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

Last updated at Posted at 2023-12-10

この記事はOpenCV Advent Calendar 2023の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が推奨となっている。しかしながら、資金難でこちらも苦しんでいる…

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

wikipediaのlossless jpegに詳しく記載がされている。

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

◯IMWRITEのフラグを新規追加する

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

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の中で、パラメータ解析している部分で、指定されたパラメータ拾う。

+
+            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の定義を立てる。

+#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 を使っているとこの関数が存在しないので、ここは対応しておくこと。

@@ -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

TP=0
image.png

TP=4
image.png

TP=5
image.png

TP=6
image.png

TP=7 もはやPC9801で見ているようだ…
image.png

■茶番2

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

■まとめ

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

■ 修正全体

全DIFF
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
13
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
5