Add vanilla CALIC algorithm Change-Id: I265de289292d8bd913b6ac04b92b9d94e6d83fc6 Reviewed-on: https://chromium-review.googlesource.com/c/codecs/libwebp2/+/6650922 Reviewed-by: Yannis Guyon <yguyon@google.com> Tested-by: WebM Builds <builds@webmproject.org>
diff --git a/CMakeLists.txt b/CMakeLists.txt index e8b749c..2fdb84a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt
@@ -415,6 +415,8 @@ ${CMAKE_CURRENT_SOURCE_DIR}/src/enc/trellis.cc ${CMAKE_CURRENT_SOURCE_DIR}/src/enc/writer_enc.cc # lossless + ${CMAKE_CURRENT_SOURCE_DIR}/src/common/lossless/calic.cc + ${CMAKE_CURRENT_SOURCE_DIR}/src/common/lossless/calic.h ${CMAKE_CURRENT_SOURCE_DIR}/src/common/lossless/color_cache.cc ${CMAKE_CURRENT_SOURCE_DIR}/src/common/lossless/color_cache.h ${CMAKE_CURRENT_SOURCE_DIR}/src/common/lossless/scp.cc
diff --git a/src/common/lossless/calic.cc b/src/common/lossless/calic.cc new file mode 100644 index 0000000..aee3752 --- /dev/null +++ b/src/common/lossless/calic.cc
@@ -0,0 +1,227 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ----------------------------------------------------------------------------- +// +// Common CALIC code from "Context-Based, Adaptive, Lossless Image Coding" +// IEEE TRANSACTIONS ON COMMUNICATIONS, VOL. 45, NO. 4, APRIL 1997 +// by Xiaolin Wu and Nasir Memon +// +// Author: Vincent Rabaud (vrabaud@google.com) + +#include "src/common/lossless/calic.h" + +#include <algorithm> +#include <cassert> +#include <cstdint> +#include <cstdlib> + +#include "src/common/lossless/plane.h" +#include "src/dsp/math.h" +#include "src/utils/utils.h" +#include "src/wp2/base.h" + +namespace WP2L { +namespace calic { + +WP2Status CalicState::Init(uint32_t width, uint32_t height, + Quantization quantization) { + WP2_CHECK_STATUS(WP2L::PlaneCodec::Init(width, height)); + WP2_CHECK_ALLOC_OK(curr_diff_col_.resize(width)); + WP2_CHECK_ALLOC_OK(curr_diff_row_.resize(width)); + WP2_CHECK_ALLOC_OK(prev_diff_col_.resize(width)); + WP2_CHECK_ALLOC_OK(prev_diff_row_.resize(width)); + + // Initialize the quantized delta table. + switch (quantization) { + case Quantization::kOriginal: + WP2_CHECK_ALLOC_OK(quantized_delta_.resize(141)); + std::fill(&quantized_delta_[0], &quantized_delta_[5], 0); + std::fill(&quantized_delta_[5], &quantized_delta_[15], 1); + std::fill(&quantized_delta_[15], &quantized_delta_[25], 2); + std::fill(&quantized_delta_[25], &quantized_delta_[42], 3); + std::fill(&quantized_delta_[42], &quantized_delta_[60], 4); + std::fill(&quantized_delta_[60], &quantized_delta_[85], 5); + std::fill(&quantized_delta_[85], &quantized_delta_[140], 6); + quantized_delta_[140] = 7; + break; + case Quantization::kNum: + assert(false); + return WP2_STATUS_INVALID_PARAMETER; + } + if (quantized_delta_.back() + 1 != GetNumContexts(quantization)) { + return WP2_STATUS_INVALID_PARAMETER; + } + + const int kSize = ((quantized_delta_.back() + 1) / 2) * (1 << 8); + WP2_CHECK_ALLOC_OK(mean_errors_.resize(kSize)); + WP2_CHECK_ALLOC_OK(s_errors_.resize(kSize)); + WP2_CHECK_ALLOC_OK(n_errors_.resize(kSize)); + WP2_CHECK_STATUS(Reset()); + + return WP2_STATUS_OK; +} + +void CalicState::SetMinMax(int16_t min_tpv, int16_t max_tpv) { + min_tpv_ = min_tpv; + max_tpv_ = max_tpv; + default_value_ = (min_tpv + max_tpv + 1) / 2; +} + +uint8_t CalicState::GetNumContexts(Quantization quantization) { + switch (quantization) { + case Quantization::kOriginal: + return 8; + break; + case Quantization::kNum: + default: + assert(false); + return WP2_STATUS_INVALID_PARAMETER; + } +} + +WP2Status CalicState::Reset() { + std::fill(mean_errors_.begin(), mean_errors_.end(), 0); + std::fill(s_errors_.begin(), s_errors_.end(), 0); + std::fill(n_errors_.begin(), n_errors_.end(), 0); + prev_pred_error_ = 0; + return WP2_STATUS_OK; +} + +void CalicState::StartProcessingLine(uint32_t y, const int16_t* row) { + PlaneCodec::StartProcessingLine(y, row); + + curr_diff_row_.swap(prev_diff_row_); + curr_diff_col_.swap(prev_diff_col_); +} + +int16_t CalicState::PredictGAP(uint16_t dh, uint16_t dv, int16_t w, int16_t n, + int16_t ne, int16_t nw) { + int16_t pred; + if (dv - dh > 80) { + // Sharp horizontal edge. + pred = w; + } else if (dv - dh < -80) { + // Sharp vertical edge. + pred = n; + } else { + int16_t p = (w + n) / 2 + (ne - nw) / 4; + if (dv - dh > 32) { + // Horizontal edge. + p = (p + w) / 2; + } else if (dv - dh > 8) { + // Weak horizontal edge. + p = (3 * p + w) / 4; + } else if (dv - dh < -32) { + // Vertical edge. + p = (p + n) / 2; + } else if (dv - dh < -8) { + // Weak vertical edge. + p = (3 * p + n) / 4; + } + pred = p; + } + return pred; +} + +void CalicState::Predict(uint32_t x, int16_t& prediction, uint8_t& ctxt, + bool& is_mean_error_negative) { + // Section 3: GAP, Gradient-Adjusted Prediction. + uint16_t dh = 0, dv = 0; + // Edges are not properly defined in the paper. + if (x > 0) { + dh = curr_diff_col_[x - 1]; + dv = curr_diff_row_[x - 1]; + } else { + dh = dv = 0; + } + if (y_ > 0) { + dh += prev_diff_col_[x]; + dv += prev_diff_row_[x]; + if (x + 1 < width_) { + dh += prev_diff_col_[x + 1]; + dv += prev_diff_row_[x + 1]; + } + } + + // Edge conditions are not specified by the paper so we take the closest + // physical value, or the default one if none. + const int16_t w = x > 0 ? row_[x - 1] : y_ > 0 ? row_p_[0] : default_value_; + const int16_t n = y_ > 0 ? row_p_[x] : w; + const int16_t ne = x + 1 < width_ && y_ > 0 ? row_p_[x + 1] : n; + const int16_t nw = x > 0 && y_ > 0 ? row_p_[x - 1] : n; + pred_ = PredictGAP(dh, dv, w, n, ne, nw); + pred_ = std::clamp(pred_, min_tpv_, max_tpv_); + + // Section 4: Coding Context Selection and Quantization. + static constexpr int kA = 1, kB = 1, kC = 2; + const size_t delta = kA * dh + kB * dv + kC * std::abs(prev_pred_error_); + const uint8_t quantized_delta = delta >= quantized_delta_.size() + ? quantized_delta_.back() + : quantized_delta_[delta]; + + // Section 5.A: Formation and Quantization of Contexts. + const int16_t nn = y_ > 1 ? row_pp_[x] : n; + const int16_t ww = x > 1 ? row_[x - 2] : w; + uint8_t b = 0; + b |= (n < pred_) << 0; + b |= (w < pred_) << 1; + b |= (nw < pred_) << 2; + b |= (ne < pred_) << 3; + b |= (nn < pred_) << 4; + b |= (ww < pred_) << 5; + b |= ((2 * n - nn) < pred_) << 6; + b |= ((2 * w - ww) < pred_) << 7; + compound_context_ = ((quantized_delta / 2) << 8) | b; + + // Section 5.D: Error Feedback. + const auto mean_error = mean_errors_[compound_context_]; + is_mean_error_negative = mean_error < 0; + const int16_t rounded_abs_residual = + WP2::RightShiftRound(std::abs(mean_error), kPrecisionBits); + prediction = + pred_ + (mean_error >= 0 ? rounded_abs_residual : -rounded_abs_residual); + prediction = std::clamp(prediction, min_tpv_, max_tpv_); + + ctxt = quantized_delta; +} + +void CalicState::Update(uint32_t x, int16_t tpv) { + if (x > 0) { + curr_diff_col_[x] = std::abs(tpv - row_[x - 1]); + } else { + curr_diff_col_[0] = 0; + } + if (y_ > 0) { + curr_diff_row_[x] = std::abs(row_[x] - row_p_[x]); + } else { + curr_diff_row_[x] = 0; + } + prev_pred_error_ = tpv - pred_; + + // Section 5.C: Estimation of Error Magnitude Within a Context. + // Use the final prediction and not the original one (cf section 5.D). + s_errors_[compound_context_] += tpv - pred_; + ++n_errors_[compound_context_]; + if (n_errors_[compound_context_] >= 128) { + s_errors_[compound_context_] /= 2; + n_errors_[compound_context_] = 64; + } + + mean_errors_[compound_context_] = WP2::DivRound( + static_cast<int32_t>(s_errors_[compound_context_]) << kPrecisionBits, + static_cast<int32_t>(n_errors_[compound_context_])); +} + +} // namespace calic +} // namespace WP2L
diff --git a/src/common/lossless/calic.h b/src/common/lossless/calic.h new file mode 100644 index 0000000..a436442 --- /dev/null +++ b/src/common/lossless/calic.h
@@ -0,0 +1,89 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ----------------------------------------------------------------------------- +// +// Implementation of the CALIC algorithm. +// +// Author: Vincent Rabaud (vrabaud@google.com) + +#ifndef WP2_COMMON_LOSSLESS_CALIC_H_ +#define WP2_COMMON_LOSSLESS_CALIC_H_ + +#include <cstdint> + +#include "src/common/lossless/plane.h" +#include "src/utils/vector.h" +#include "src/wp2/base.h" + +namespace WP2L { +namespace calic { + +// Class implementing the CALIC algorithm from Xiaolin Wu and Nasir Memon. +class CalicState : public ::WP2L::PlaneCodec { + public: + // This is part of the format. + enum class Quantization { kOriginal, kNum }; + + WP2Status Init(uint32_t width, uint32_t height, Quantization quantization); + WP2Status Reset() override; + void StartProcessingLine(uint32_t y, const int16_t* row) override; + + // Sets the minimum and maximum true pixel inclusive values in the image. + void SetMinMax(int16_t min_tpv, int16_t max_tpv); + // Get the number of contexts for the given quantization method. + static uint8_t GetNumContexts(Quantization quantization); + // Predicts the pixel value at position x, as well as the context `ctxt` and + // whether the mean error is negative. + void Predict(uint32_t x, int16_t& prediction, uint8_t& ctxt, + bool& is_mean_error_negative); + // Updates the internals after we know the true pixel value `tpv` at pixel x. + void Update(uint32_t x, int16_t tpv); + + private: + using ::WP2L::PlaneCodec::Init; + // Number of bits used to perform operations with. + static inline constexpr int kPrecisionBits = 8; + + // Predicts the pixel value using the GAP method. The horizontal and vertical + // gradients are given as well as the context of the 4 neighbors. + static int16_t PredictGAP(uint16_t dh, uint16_t dv, int16_t w, int16_t n, + int16_t ne, int16_t nw); + + // The minimum and maximum true pixel values in the image. + int16_t min_tpv_, max_tpv_; + // The default value to use when there is no information: the mean of the + // minimum and maximum true pixel values. + int16_t default_value_; + + // The current prediction. + int16_t pred_; + // The previous error between the prediction and the true pixel value. + int16_t prev_pred_error_; + // Vectors containing the differences between the current and previous cols or + // rows, in the current or previous row. + WP2::Vector_u16 curr_diff_col_, curr_diff_row_, prev_diff_col_, + prev_diff_row_; + // Accumulator of the errors with precision kPrecisionBits. + WP2::Vector_s16 s_errors_; + WP2::Vector_u8 n_errors_; + // The mean error, with precision kPrecisionBits. + WP2::Vector_s32 mean_errors_; + WP2::Vector_u8 quantized_delta_; + uint16_t compound_context_; +}; + +} // namespace calic +} // namespace WP2L + +#endif // WP2_COMMON_LOSSLESS_CALIC_H_
diff --git a/src/common/lossless/plane.h b/src/common/lossless/plane.h index b2851f2..c3aff20 100644 --- a/src/common/lossless/plane.h +++ b/src/common/lossless/plane.h
@@ -66,7 +66,7 @@ class PlaneCodec { public: - enum class Method { kScpClassical, kScpJxl, kNum }; + enum class Method { kScpClassical, kScpJxl, kCalic, kNum }; virtual ~PlaneCodec() = default;
diff --git a/src/dec/lossless/losslessi_dec.cc b/src/dec/lossless/losslessi_dec.cc index 0e6c875..27e2216 100644 --- a/src/dec/lossless/losslessi_dec.cc +++ b/src/dec/lossless/losslessi_dec.cc
@@ -632,7 +632,8 @@ WP2_CHECK_STATUS(DecodeGroup4(width, last_row, bits_per_pixel, &src)); } else if (encoding_algorithm_ == EncodingAlgorithm::kLZW) { WP2_CHECK_STATUS(DecodeLZW(width, last_row, bits_per_pixel, &src)); - } else if (encoding_algorithm_ == EncodingAlgorithm::kSCP) { + } else if (encoding_algorithm_ == EncodingAlgorithm::kSCP || + encoding_algorithm_ == EncodingAlgorithm::kCALIC) { WP2_CHECK_STATUS( DecodePlaneCodec(width, last_row, scp_global_, bits_per_pixel, &src)); } else { @@ -1143,7 +1144,8 @@ } dec_->PopDebugPrefix(); return WP2_STATUS_OK; - } else if (encoding_algorithm_ == EncodingAlgorithm::kSCP) { + } else if (encoding_algorithm_ == EncodingAlgorithm::kSCP || + encoding_algorithm_ == EncodingAlgorithm::kCALIC) { for (int c = gparams_->has_alpha_ ? 0 : 1; c < 4; ++c) { scp_global_.max_tpv[c] = c == 0 ? WP2::kAlphaMax
diff --git a/src/dec/lossless/losslessi_dec.h b/src/dec/lossless/losslessi_dec.h index 359f501..884f421 100644 --- a/src/dec/lossless/losslessi_dec.h +++ b/src/dec/lossless/losslessi_dec.h
@@ -23,6 +23,7 @@ #include <array> #include <cstdint> +#include "src/common/lossless/calic.h" #include "src/common/lossless/color_cache.h" #include "src/common/lossless/plane.h" #include "src/common/lossless/scp.h" @@ -86,6 +87,7 @@ uint32_t maxerr_shift; int jxl_header_index; scp::JxlHeader jxl_header; + calic::CalicState::Quantization calic_quantization; }; class Decoder {
diff --git a/src/dec/lossless/plane_dec.cc b/src/dec/lossless/plane_dec.cc index 34c9a4a..b002cef 100644 --- a/src/dec/lossless/plane_dec.cc +++ b/src/dec/lossless/plane_dec.cc
@@ -24,6 +24,7 @@ #include <cstdio> #include <cstring> +#include "src/common/lossless/calic.h" #include "src/common/lossless/plane.h" #include "src/common/lossless/scp.h" #include "src/common/symbols.h" @@ -38,6 +39,69 @@ namespace WP2L { namespace scp { +//////////////////////////////////////////////////////////////////////////////// + +class CalicDecState : public ::WP2L::calic::CalicState { + public: + int16_t CalcTPVFromPredictionAndDistance(int16_t p, + bool is_mean_error_negative, + int16_t d, int16_t min, + int16_t max) { + // Compute the maximum error for which the sign is not forced from the + // prediction. E.g., if the prediction is 10 and the value is in [0, 100], + // the signed error can only be in [-10, 10]. Beyond that, the error is + // positive in [0, 80]. + const int16_t max_signed_error = std::min(max - p, p - min); + int16_t res; + if (d <= 2 * max_signed_error) { + res = d % 2 == 0 ? -d / 2 : (d + 1) / 2; + // End of section 5.D: Error Feedback. + if (is_mean_error_negative) res = -res; + p += res; + } else { + res = d - max_signed_error; + if (p + res > max) { + p -= res; + } else { + p += res; + } + } + return p; + } + + WP2Status DecompressPlane(int plane_to_decompress, const ScpGlobal& global, + WP2::SymbolReader& sr, ImagePlanes& img) { + const int16_t min_tpv = global.min_tpv[plane_to_decompress]; + const int16_t max_tpv = global.max_tpv[plane_to_decompress]; + SetMinMax(min_tpv, max_tpv); + + for (size_t y = 0; y < img.height(); ++y) { + int16_t* row = img.PlaneRow(plane_to_decompress, y); + StartProcessingLine(y, row); + + for (size_t x = 0; x < img.width(); ++x) { + // Predict. + uint8_t ctxt; + bool is_mean_error_negative; + int16_t prediction; + Predict(x, prediction, ctxt, is_mean_error_negative); + + // Compute the true pixel value. + const int16_t residual = + sr.Read(/*sym=*/plane_to_decompress, ctxt, "sym"); + + row[x] = CalcTPVFromPredictionAndDistance( + prediction, is_mean_error_negative, residual, min_tpv, max_tpv); + WP2_CHECK_OK(row[x] >= min_tpv && row[x] <= max_tpv, + WP2_STATUS_BITSTREAM_ERROR); + + Update(x, row[x]); + } + } + + return WP2_STATUS_OK; + } +}; //////////////////////////////////////////////////////////////////////////////// @@ -194,6 +258,14 @@ } num_contexts = scp::JXLState::GetNumContexts(); break; + case PlaneCodec::Method::kCalic: + global->calic_quantization = + static_cast<calic::CalicState::Quantization>(dec_->ReadRValue( + static_cast<int>(calic::CalicState::Quantization::kNum), + "quantization")); + num_contexts = + calic::CalicState::GetNumContexts(global->calic_quantization); + break; case PlaneCodec::Method::kNum: default: assert(false); @@ -251,6 +323,13 @@ state, hdr_.sr_, src)); break; } + case PlaneCodec::Method::kCalic: { + scp::CalicDecState state; + WP2_CHECK_STATUS(state.Init(width, height, global.calic_quantization)); + WP2_CHECK_STATUS(Decompress(global, width, height, gparams_->has_alpha_, + state, hdr_.sr_, src)); + break; + } case PlaneCodec::Method::kNum: default: assert(false);
diff --git a/src/enc/lossless/analysisl_enc.cc b/src/enc/lossless/analysisl_enc.cc index 3dc1db4..9e7e990 100644 --- a/src/enc/lossless/analysisl_enc.cc +++ b/src/enc/lossless/analysisl_enc.cc
@@ -531,6 +531,48 @@ return WP2_STATUS_OK; } + // TODO(vrabaud): Enable for all efforts once it gets better. + if (WP2Formatbpc(pic.format()) == 8 && + !WP2IsPremultiplied(encoder.pic_.format()) && + encoder.config_.lossless_algorithm == WP2L::EncodingAlgorithm::kCALIC) { + // Add a few more than the predefined ones. + std::array<std::array<uint32_t, 2>, kCommonCrossColorIndices.size() + 4> + y_uv_indices; + for (uint32_t i = 0; i < kCommonCrossColorIndices.size(); ++i) { + y_uv_indices[i] = {kCommonCrossColorIndices[i].first, + kCommonCrossColorIndices[i].second}; + } + // Taken from Table VI in "Multiplierless reversible colour transforms + // and their automatic selection for image data compression" by Tilo Strutz. + y_uv_indices[kCommonCrossColorIndices.size() + 0] = {4, 4}; + y_uv_indices[kCommonCrossColorIndices.size() + 1] = {7, 7}; + y_uv_indices[kCommonCrossColorIndices.size() + 2] = {9, 10}; + y_uv_indices[kCommonCrossColorIndices.size() + 3] = {1, 10}; + + for (const auto& indices : y_uv_indices) { + WP2_CHECK_ALLOC_OK(configs->push_back(CrunchConfig())); + auto& config = configs->back(); + WP2_CHECK_STATUS(FindEncodingRecipe( + WP2L::EncodingAlgorithm::kCALIC, &config, + TransformType::kCrossColorGlobal, TransformType::kNormalizeChannels)); + config.transforms[0].cc_global_y_index = indices[0]; + config.transforms[0].cc_global_uv_index = indices[1]; + config.use_premultiplied = can_premultiply; + } + // Add with no channel decorrelation. + WP2_CHECK_ALLOC_OK(configs->push_back(CrunchConfig())); + auto& config = configs->back(); + WP2_CHECK_STATUS(FindEncodingRecipe(WP2L::EncodingAlgorithm::kCALIC, + &config, + TransformType::kNormalizeChannels)); + config.use_premultiplied = can_premultiply; + + if (encoder.config_.lossless_algorithm == WP2L::EncodingAlgorithm::kCALIC) { + return WP2_STATUS_OK; + } + } + + // TODO(vrabaud): Disable this once CALIC gets better. if (WP2Formatbpc(pic.format()) == 8 && !WP2IsPremultiplied(encoder.pic_.format()) && (encoder.config_.lossless_algorithm == WP2L::EncodingAlgorithm::kSCP ||
diff --git a/src/enc/lossless/losslessi_enc.cc b/src/enc/lossless/losslessi_enc.cc index f129deb..b473c04 100644 --- a/src/enc/lossless/losslessi_enc.cc +++ b/src/enc/lossless/losslessi_enc.cc
@@ -485,14 +485,22 @@ } // Check if SCP is used. + std::array<int32_t, 4> minima_range, maxima_range; if (config.algorithm == EncodingAlgorithm::kSCP) { - std::array<int32_t, 4> minima_range, maxima_range; WP2_CHECK_STATUS(GetARGBRanges(config.transforms, symbols_info_init.SampleFormat(), minima_range, maxima_range)); - WP2_CHECK_STATUS(PlaneCodecEncode(argb_buffer_, config_.effort, - minima_range, maxima_range, *enc_init, - encode_info)); + WP2_CHECK_STATUS(ScpEncode(argb_buffer_, config_.effort, minima_range, + maxima_range, *enc_init, encode_info)); + WP2_CHECK_STATUS(progress.AdvanceBy(1.)); + return WP2_STATUS_OK; + } + if (config.algorithm == EncodingAlgorithm::kCALIC) { + WP2_CHECK_STATUS(GetARGBRanges(config.transforms, + symbols_info_init.SampleFormat(), + minima_range, maxima_range)); + WP2_CHECK_STATUS(CalicEncode(argb_buffer_, config_.effort, minima_range, + maxima_range, *enc_init, encode_info)); WP2_CHECK_STATUS(progress.AdvanceBy(1.)); return WP2_STATUS_OK; } @@ -1135,6 +1143,9 @@ if (config->algorithm == EncodingAlgorithm::kSCP) { return WP2_STATUS_OK; } + if (config->algorithm == EncodingAlgorithm::kCALIC) { + return WP2_STATUS_OK; + } if (has_alpha_ && image_is_premultiplied_) { // The lossless encoder may use unmultiplied samples for better compression,
diff --git a/src/enc/lossless/losslessi_enc.h b/src/enc/lossless/losslessi_enc.h index abe9ef4..19047ff 100644 --- a/src/enc/lossless/losslessi_enc.h +++ b/src/enc/lossless/losslessi_enc.h
@@ -314,10 +314,18 @@ //------------------------------------------------------------------------------ // Self-correcting predictor. -WP2Status PlaneCodecEncode(const Buffer_s16& img, int effort, - const std::array<int32_t, 4>& minima_range, - const std::array<int32_t, 4>& maxima_range, - WP2::ANSEnc& enc, EncodeInfo* encode_info); +WP2Status ScpEncode(const Buffer_s16& img, int effort, + const std::array<int32_t, 4>& minima_range, + const std::array<int32_t, 4>& maxima_range, + WP2::ANSEnc& enc, EncodeInfo* encode_info); + +//------------------------------------------------------------------------------ +// CALIC. + +WP2Status CalicEncode(const Buffer_s16& img, int effort, + const std::array<int32_t, 4>& minima_range, + const std::array<int32_t, 4>& maxima_range, + WP2::ANSEnc& enc, EncodeInfo* encode_info); } // namespace WP2L
diff --git a/src/enc/lossless/plane_enc.cc b/src/enc/lossless/plane_enc.cc index 2cf5622..c890559 100644 --- a/src/enc/lossless/plane_enc.cc +++ b/src/enc/lossless/plane_enc.cc
@@ -28,6 +28,7 @@ #include <limits> #include <type_traits> +#include "src/common/lossless/calic.h" #include "src/common/lossless/plane.h" #include "src/common/lossless/scp.h" #include "src/dsp/lossless/encl_dsp.h" @@ -61,6 +62,76 @@ //////////////////////////////////////////////////////////////////////////////// +// CALIC based encoder. +class CalicEncState : public ::WP2L::calic::CalicState { + public: + struct Config { + Quantization quantization = Quantization::kOriginal; + }; + + void SetConfig(const Config& config) { config_ = config; } + + void EncodeHeader(WP2::ANSEncBase& enc) { + enc.PutRValue(static_cast<uint32_t>(config_.quantization), + static_cast<uint32_t>(Quantization::kNum), "quantization"); + } + + // p = prediction, v = true pixel value, m = maximum true pixel value + int16_t CalcDistanceFromPredictionAndTPV(int16_t p, + bool is_mean_error_negative, + int16_t v, int16_t min, + int16_t max) { + const int16_t max_signed_error = std::min(max - p, p - min); + int16_t res = v - p; + int16_t d; + if (std::abs(res) <= max_signed_error) { + // End of section 5.D: Error Feedback. + if (is_mean_error_negative) res = -res; + d = res > 0 ? 2 * res - 1 : -2 * res; + } else { + d = std::abs(res) + max_signed_error; + } + return d; + } + + WP2Status CompressPlane(const ImagePlanes& img, int plane_to_compress, + int16_t min_tpv, int16_t max_tpv, + Encoding* encoding) { + encoding->tokens.clear(); + SetMinMax(min_tpv, max_tpv); + + for (size_t y = 0; y < img.height(); ++y) { + const int16_t* row = img.PlaneRow(plane_to_compress, y); + StartProcessingLine(y, row); + + for (size_t x = 0; x < img.width(); ++x) { + uint8_t ctxt; + bool is_mean_error_negative; + int16_t prediction; + Predict(x, prediction, ctxt, is_mean_error_negative); + + WP2_CHECK_ALLOC_OK(encoding->tokens.push_back(Token( + ctxt, + CalcDistanceFromPredictionAndTPV(prediction, is_mean_error_negative, + row[x], min_tpv, max_tpv)))); + + Update(x, row[x]); + } + } + + return WP2_STATUS_OK; + } + + int GetNumContexts() const { + return calic::CalicState::GetNumContexts(config_.quantization); + } + + private: + Config config_; +}; + +//////////////////////////////////////////////////////////////////////////////// + // Encoder for the classical SCP. // SetConfig must be called after Init to be properly usable. class ClassicalEncState : public ClassicalState { @@ -327,19 +398,19 @@ return WP2_STATUS_OK; } -} // namespace scp -WP2Status PlaneCodecEncode(const Buffer_s16& img, int effort, - const std::array<int32_t, 4>& minima_range, - const std::array<int32_t, 4>& maxima_range, - WP2::ANSEnc& enc, EncodeInfo* encode_info) { +static WP2Status PrepareData(const Buffer_s16& img, + const std::array<int32_t, 4>& minima_range, + const std::array<int32_t, 4>& maxima_range, + std::array<int16_t, 4>& min_tpv, + std::array<int16_t, 4>& max_tpv, WP2::ANSEnc& enc, + WP2L::ImagePlanes& img_planes) { if (img.channel_bits != 8) return WP2_STATUS_INVALID_PARAMETER; const size_t width = img.width; const size_t height = img.height; // The code modifies the image for palette so must copy for now. - WP2L::ImagePlanes img_planes; WP2_CHECK_STATUS(img_planes.Create(width, height, img.has_alpha)); for (int c = (img.has_alpha ? 0 : 1); c < 4; c++) { for (size_t y = 0; y < height; ++y) { @@ -349,7 +420,6 @@ } } } - std::array<int16_t, 4> min_tpv, max_tpv; FindExtrema(img.GetRow(0), width, height, img.has_alpha, min_tpv, max_tpv); enc.AddDebugPrefix("GlobalHeader"); for (int c = img.has_alpha ? 0 : 1; c < 4; ++c) { @@ -358,6 +428,23 @@ } enc.PopDebugPrefix(); + return WP2_STATUS_OK; +} + +} // namespace scp + +WP2Status ScpEncode(const Buffer_s16& img, int effort, + const std::array<int32_t, 4>& minima_range, + const std::array<int32_t, 4>& maxima_range, + WP2::ANSEnc& enc, EncodeInfo* encode_info) { + const size_t width = img.width; + const size_t height = img.height; + + WP2L::ImagePlanes img_planes; + std::array<int16_t, 4> min_tpv, max_tpv; + WP2_CHECK_STATUS(scp::PrepareData(img, minima_range, maxima_range, min_tpv, + max_tpv, enc, img_planes)); + float cost_best = std::numeric_limits<float>::max(); WP2::ANSDictionaries dicts; @@ -481,4 +568,26 @@ return WP2_STATUS_OK; } +WP2Status CalicEncode(const Buffer_s16& img, int effort, + const std::array<int32_t, 4>& minima_range, + const std::array<int32_t, 4>& maxima_range, + WP2::ANSEnc& enc, EncodeInfo* encode_info) { + WP2L::ImagePlanes img_planes; + std::array<int16_t, 4> min_tpv, max_tpv; + WP2_CHECK_STATUS(scp::PrepareData(img, minima_range, maxima_range, min_tpv, + max_tpv, enc, img_planes)); + WP2::ANSDictionaries dicts; + + // Encode using CALIC. + scp::CalicEncState state; + scp::CalicEncState::Config calic_config_best; + calic_config_best.quantization = calic::CalicState::Quantization::kOriginal; + state.SetConfig(calic_config_best); + WP2_CHECK_STATUS( + state.Init(img.width, img.height, calic_config_best.quantization)); + WP2_CHECK_STATUS(Compress(img_planes, min_tpv, max_tpv, effort, enc, state, + dicts, encode_info)); + return WP2_STATUS_OK; +} + } // namespace WP2L
diff --git a/src/wp2/format_constants.h b/src/wp2/format_constants.h index e082ac1..ad78a83 100644 --- a/src/wp2/format_constants.h +++ b/src/wp2/format_constants.h
@@ -370,6 +370,7 @@ kLZW, // Self-correcting predictor. kSCP, + kCALIC, kNum, kDefault = kNum }; @@ -405,6 +406,12 @@ {TransformType::kNum, TransformType::kNum, TransformType::kNum}}, {EncodingAlgorithm::kLZW, {TransformType::kNum, TransformType::kNum, TransformType::kNum}}, + {EncodingAlgorithm::kCALIC, + {TransformType::kCrossColorGlobal, TransformType::kNormalizeChannels, + TransformType::kNum}}, + {EncodingAlgorithm::kCALIC, + {TransformType::kNormalizeChannels, TransformType::kNum, + TransformType::kNum}}, {EncodingAlgorithm::kSCP, {TransformType::kNormalizeChannels, TransformType::kNum, TransformType::kNum}},
diff --git a/tests/lossless/test_plane_codecs.cc b/tests/lossless/test_plane_codecs.cc index dc4fc69..b751a1d 100644 --- a/tests/lossless/test_plane_codecs.cc +++ b/tests/lossless/test_plane_codecs.cc
@@ -104,7 +104,8 @@ std::array<int, 2>{555, 577}, std::array<int, 2>{577, 555}, std::array<int, 2>{5, 800}), - testing::Values(WP2L::EncodingAlgorithm::kSCP))); + testing::Values(WP2L::EncodingAlgorithm::kSCP, + WP2L::EncodingAlgorithm::kCALIC))); class LosslessTestFile : public testing::TestWithParam< std::tuple<std::string, WP2L::EncodingAlgorithm>> { @@ -126,7 +127,8 @@ LosslessTestInstantiation, LosslessTestFile, testing::Combine(testing::Values("background.png", "logo.png", "source1_64x48.png"), - testing::Values(WP2L::EncodingAlgorithm::kSCP))); + testing::Values(WP2L::EncodingAlgorithm::kSCP, + WP2L::EncodingAlgorithm::kCALIC))); ////////////////////////////////////////////////////////////////////////////////