この記事は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 |
全体画像はこんな感じ。このうち、木の幹部分に注目。
■茶番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