Add a transform that maps each channel to an index
It is not a very useful transform for now.
Change-Id: I759989fc340ed82534bb813c3896e9f88754c0b5
Reviewed-on: https://chromium-review.googlesource.com/c/codecs/libwebp2/+/6249119
Tested-by: WebM Builds <builds@webmproject.org>
Reviewed-by: Yannis Guyon <yguyon@google.com>
diff --git a/src/common/lossless/transforms.cc b/src/common/lossless/transforms.cc
index 88a11b8..107ba81 100644
--- a/src/common/lossless/transforms.cc
+++ b/src/common/lossless/transforms.cc
@@ -201,6 +201,27 @@
// Only the color index in [0, num_colors-1] is stored in G.
segments[2] = {0, static_cast<int32_t>(header.indexing_num_colors) - 1};
break;
+ case TransformType::kNormalizeChannels: {
+ if (header.normalize_channels_has_palette) {
+ for (uint32_t c = 0; c < 4; ++c) {
+ if (header.normalize_channels_num_colors[c] > 0) {
+ segments[c] = {0, static_cast<int32_t>(
+ header.normalize_channels_num_colors[c]) -
+ 1};
+ } else {
+ if (c != 0) return WP2_STATUS_INVALID_PARAMETER;
+ segments[c] = {0, 0};
+ }
+ }
+ } else {
+ for (uint32_t c = 0; c < 4; ++c) {
+ segments[c] =
+ segments[c] + Segment{-header.normalize_channels_min[c],
+ -header.normalize_channels_min[c]};
+ }
+ }
+ break;
+ }
case TransformType::kNum:
assert(false);
}
diff --git a/src/common/vdebug.cc b/src/common/vdebug.cc
index b12de58..bfc3f0b 100644
--- a/src/common/vdebug.cc
+++ b/src/common/vdebug.cc
@@ -2782,8 +2782,8 @@
}
static const char* const kTransformNames[] = {
- "Predictor", "Predictor with sub", "CrossColor", "CrossColorGlobal",
- "YCoCgR", "SubstractGreen", "ColorIndexing"};
+ "Predictor", "Predictor with sub", "CrossColor", "CrossColorGlobal",
+ "YCoCgR", "SubstractGreen", "ColorIndexing", "NormalizeChannels"};
STATIC_ASSERT_ARRAY_SIZE(kTransformNames, (uint32_t)TransformType::kNum);
static const char* const kPredictorNames[] = {"Black",
"180",
diff --git a/src/dec/lossless/losslessi_dec.cc b/src/dec/lossless/losslessi_dec.cc
index a426477..bd56df9 100644
--- a/src/dec/lossless/losslessi_dec.cc
+++ b/src/dec/lossless/losslessi_dec.cc
@@ -832,7 +832,7 @@
}
WP2Status Decoder::ReadTransform(uint32_t width, uint32_t height,
- TransformType type,
+ TransformType type, int index,
Transform* const transform) {
transform->header_.type = type;
transform->width_pic_ = width;
@@ -927,6 +927,35 @@
transform->header_.indexing_num_colors = transform->data_.size() / 4;
break;
}
+ case TransformType::kNormalizeChannels: {
+ WP2::ANSDebugPrefix prefix(dec_, "normalize_channels");
+ std::array<int32_t, 4> minima_range, maxima_range;
+ std::array<TransformHeader, kPossibleTransformCombinationSize> headers;
+ for (int i = 0; i < index; ++i) {
+ headers[i] = transforms_[i].header_;
+ }
+ WP2_CHECK_STATUS(GetARGBRanges(headers, hdr_.symbols_info_.SampleFormat(),
+ minima_range, maxima_range, index));
+ for (int c = gparams_->has_alpha_ ? 0 : 1; c < 4; ++c) {
+ transform->header_.normalize_channels_min[c] =
+ dec_->ReadRange(minima_range[c], maxima_range[c], "min");
+ }
+ transform->header_.normalize_channels_has_palette =
+ dec_->ReadBool("has_palette");
+ if (transform->header_.normalize_channels_has_palette) {
+ for (int c = gparams_->has_alpha_ ? 0 : 1; c < 4; ++c) {
+ const uint32_t range = 1 + maxima_range[c] - minima_range[c];
+ const uint32_t mapping_size = dec_->ReadRange(1, range, "size");
+ transform->header_.normalize_channels_num_colors[c] = mapping_size;
+ WP2_CHECK_ALLOC_OK(
+ transform->normalize_channels_mapping_[c].resize(mapping_size));
+ WP2_CHECK_STATUS(
+ LoadMapping(dec_, mapping_size, range,
+ transform->normalize_channels_mapping_[c].data()));
+ }
+ }
+ break;
+ }
case TransformType::kSubtractGreen:
case TransformType::kYCoCgR:
break;
@@ -1013,23 +1042,21 @@
dec_->AddDebugPrefix("transforms");
const uint32_t index = dec_->ReadRValue(kPossibleEncodingRecipesNum, "index");
encoding_algorithm_ = kPossibleEncodingRecipes[index].algorithm;
- const TransformType* const transforms =
- kPossibleEncodingRecipes[index].transforms;
- uint32_t num_transforms = 0;
- for (; num_transforms < kPossibleTransformCombinationSize; ++num_transforms) {
- if (transforms[num_transforms] == TransformType::kNum) {
- break;
- }
- }
- // Deactivate all transforms in case the decoder is re-used.
- for (Transform& t : transforms_) t.header_.type = TransformType::kNum;
- for (uint32_t i = 0; i < num_transforms; ++i) {
- TransformType type = transforms[i];
- WP2_CHECK_STATUS(ReadTransform(tile_->rect.width, tile_->rect.height, type,
- &transforms_[i]));
- WP2_CHECK_REDUCED_STATUS(
- RegisterTransformForVDebug(transforms_[i], config_.info));
+ uint32_t num_transforms = kPossibleTransformCombinationSize;
+ for (uint32_t i = 0; i < kPossibleTransformCombinationSize; ++i) {
+ const TransformType type = kPossibleEncodingRecipes[index].transforms[i];
+ Transform& transform = transforms_[i];
+ if (type == TransformType::kNum) {
+ num_transforms = std::min(num_transforms, i);
+ // Deactivate all transforms in case the decoder is re-used.
+ transform.header_.type = TransformType::kNum;
+ } else {
+ WP2_CHECK_STATUS(ReadTransform(tile_->rect.width, tile_->rect.height,
+ type, i, &transform));
+ WP2_CHECK_REDUCED_STATUS(
+ RegisterTransformForVDebug(transform, config_.info));
+ }
}
// Read headers for specific algorithms.
@@ -1103,7 +1130,8 @@
}
// Modify the main image ranges if we only use the palette.
- if (num_transforms == 1 && transforms[0] == TransformType::kColorIndexing) {
+ if (num_transforms == 1 &&
+ transforms_[0].header_.type == TransformType::kColorIndexing) {
hdr_.symbols_info_.InitAsLabelImage(tile_->rect.GetArea(), num_colors_);
} else if (num_transforms != 0) {
std::array<int32_t, 4> minima_range, maxima_range;
diff --git a/src/dec/lossless/losslessi_dec.h b/src/dec/lossless/losslessi_dec.h
index f7e8191..96911a8 100644
--- a/src/dec/lossless/losslessi_dec.h
+++ b/src/dec/lossless/losslessi_dec.h
@@ -44,12 +44,17 @@
class Transform {
public:
TransformHeader header_; // header.
- uint32_t bits_ = 0; // subsampling bits defining transform window.
uint32_t width_pic_; // initial picture width
uint32_t height_pic_; // initial picture height
- uint32_t width_ = 0; // transform width (if any)
- uint32_t height_ = 0; // transform height (if any)
- WP2::Vector_s16 data_; // transform data.
+
+ // Generic transform parameters.
+ uint32_t bits_ = 0; // subsampling bits defining transform window.
+ uint32_t width_ = 0;
+ uint32_t height_ = 0;
+ WP2::Vector_s16 data_;
+
+ // Parameters when normalizing channels
+ std::array<WP2::Vector_u16, 4> normalize_channels_mapping_;
};
class Metadata {
@@ -120,7 +125,7 @@
WP2::Planef* bits_per_pixel);
// Reads a transform of a certain size from the stream.
WP2Status ReadTransform(uint32_t width, uint32_t height, TransformType type,
- Transform* transform);
+ int index, Transform* transform);
// Visual debug. 'symbol_type' can be a SymbolType for classical algorithm,
// a Group4Mode for Group4 algorithm, or 0 for LZW algorithm.
diff --git a/src/dsp/lossless/decl_dsp.cc b/src/dsp/lossless/decl_dsp.cc
index 3ce6e0f..3e11f57 100644
--- a/src/dsp/lossless/decl_dsp.cc
+++ b/src/dsp/lossless/decl_dsp.cc
@@ -374,6 +374,28 @@
width, dst);
}
+void NormalizeChannelsInverseTransform_C(const Transform* const transform,
+ uint32_t y_start, uint32_t y_end,
+ const int16_t* src, int16_t* dst) {
+ const uint32_t width = transform->width_pic_;
+ if (transform->header_.normalize_channels_has_palette) {
+ for (const int16_t* const src_end = src + 4 * (y_end - y_start) * width;
+ src < src_end; src += 4, dst += 4) {
+ for (int c = 0; c < 4; ++c) {
+ dst[c] = transform->header_.normalize_channels_min[c] +
+ transform->normalize_channels_mapping_[c][src[c]];
+ }
+ }
+ } else {
+ for (const int16_t* const src_end = src + 4 * (y_end - y_start) * width;
+ src < src_end; src += 4, dst += 4) {
+ for (int c = 0; c < 4; ++c) {
+ dst[c] = transform->header_.normalize_channels_min[c] + src[c];
+ }
+ }
+ }
+}
+
//------------------------------------------------------------------------------
// SSE version
@@ -493,6 +515,10 @@
case TransformType::kColorIndexing:
ColorIndexInverseTransform_C(transform, row_start, row_end, in, out);
break;
+ case TransformType::kNormalizeChannels:
+ NormalizeChannelsInverseTransform_C(transform, row_start, row_end, in,
+ out);
+ break;
case TransformType::kNum:
assert(false);
}
diff --git a/src/enc/lossless/losslessi_enc.cc b/src/enc/lossless/losslessi_enc.cc
index 75081bc..93b37e6 100644
--- a/src/enc/lossless/losslessi_enc.cc
+++ b/src/enc/lossless/losslessi_enc.cc
@@ -790,6 +790,99 @@
return status;
}
+WP2Status Encoder::ApplyNormalizeChannels(
+ const std::array<int32_t, 4>& minima_range,
+ const std::array<int32_t, 4>& maxima_range, TransformHeader& header,
+ WP2::ANSEnc& enc) {
+ const WP2::ANSDebugPrefix prefix(&enc, "normalize_channels");
+ std::array<int16_t, 4> min, max;
+ const uint32_t width = argb_buffer_.width;
+ const uint32_t height = argb_buffer_.height;
+ const int first_channel = has_alpha_ ? 0 : 1;
+ for (int c = first_channel; c < 4; c++) {
+ const int16_t* const row = argb_buffer_.GetRow(0);
+ min[c] = max[c] = row[c];
+ }
+ // Figure out the min and max values.
+ for (uint32_t y = 0; y < height; ++y) {
+ const int16_t* const row = argb_buffer_.GetRow(y);
+ for (uint32_t x = 0; x < width; ++x) {
+ for (int c = first_channel; c < 4; c++) {
+ if (row[4 * x + c] < min[c]) {
+ min[c] = row[4 * x + c];
+ } else if (row[4 * x + c] > max[c]) {
+ max[c] = row[4 * x + c];
+ }
+ }
+ }
+ }
+ // Store the minimum.
+ for (int c = first_channel; c < 4; ++c) {
+ header.normalize_channels_min[c] = min[c];
+ enc.PutRange(min[c], minima_range[c], maxima_range[c], "min");
+ max[c] -= min[c];
+ }
+
+ // TODO(vrabaud) This is a basic heuristic for now.
+ constexpr uint32_t kMinSizeForPalette = 256;
+ header.normalize_channels_has_palette = width * height >= kMinSizeForPalette;
+ if (!enc.PutBool(header.normalize_channels_has_palette, "has_palette")) {
+ // Remove the min value to only have positive values.
+ for (uint32_t y = 0; y < height; ++y) {
+ int16_t* const row = argb_buffer_.GetRow(y);
+ for (uint32_t x = 0; x < width; ++x) {
+ for (int c = first_channel; c < 4; c++) row[x + c] -= min[c];
+ }
+ }
+ return WP2_STATUS_OK;
+ }
+ // Compute whether a value is present.
+ std::array<WP2::Vector_u8, 4> present;
+ for (int c = first_channel; c < 4; ++c) {
+ WP2_CHECK_ALLOC_OK(present[c].resize(max[c] + 1));
+ std::fill(present[c].begin(), present[c].end(), 0);
+ for (uint32_t y = 0; y < height; ++y) {
+ int16_t* const row = argb_buffer_.GetRow(y);
+ for (uint32_t x = 0; x < width; ++x) {
+ row[4 * x + c] -= min[c];
+ present[c][row[4 * x + c]] = 1;
+ }
+ }
+ }
+ // Convert to palette.
+ WP2::Vector_u16 palette;
+ WP2::Vector_u16 mappings;
+ for (int c = first_channel; c < 4; ++c) {
+ WP2_CHECK_ALLOC_OK(palette.resize(max[c] + 1));
+ WP2_CHECK_ALLOC_OK(mappings.resize(max[c] + 1));
+ uint32_t mapping_size = 0;
+ for (uint32_t i = 0; i < present[c].size(); ++i) {
+ if (present[c][i]) {
+ palette[i] = mapping_size;
+ mappings[mapping_size] = i;
+ ++mapping_size;
+ } else {
+ palette[i] = std::numeric_limits<uint16_t>::max();
+ }
+ }
+ // Store the mapping.
+ const uint32_t range = 1 + maxima_range[c] - minima_range[c];
+ enc.PutRange(mapping_size, 1, range, "size");
+ header.normalize_channels_num_colors[c] = mapping_size;
+ WP2::VectorNoCtor<WP2::OptimizeArrayStorageStat> stats;
+ WP2_CHECK_ALLOC_OK(stats.resize(mapping_size));
+ StoreMapping(mappings.data(), mapping_size, range, stats.data(), &enc);
+ // Apply the mapping.
+ for (uint32_t y = 0; y < height; ++y) {
+ int16_t* const row = argb_buffer_.GetRow(y);
+ for (uint32_t x = c; x < 4 * width; x += 4) {
+ row[x] = palette[row[x]];
+ }
+ }
+ }
+ return WP2_STATUS_OK;
+}
+
// -----------------------------------------------------------------------------
// Allocates the memory for argb (W x H) buffer, 2 rows of context for
@@ -974,6 +1067,16 @@
transform.cc_global_second_transform,
transform.cc_global_third_transform, enc));
break;
+ case TransformType::kNormalizeChannels: {
+ std::array<int32_t, 4> minima_range, maxima_range;
+ WP2_CHECK_STATUS(
+ GetARGBRanges(config->transforms, symbols_info_.SampleFormat(),
+ minima_range, maxima_range, /*num_transforms=*/i));
+ WP2_CHECK_STATUS(ApplyNormalizeChannels(minima_range, maxima_range,
+ transform, *enc));
+ WP2_CHECK_STATUS(progress_local.AdvanceBy(1.));
+ break;
+ }
case TransformType::kColorIndexing:
case TransformType::kNum:
assert(false);
diff --git a/src/enc/lossless/losslessi_enc.h b/src/enc/lossless/losslessi_enc.h
index 202fa72..46f59af 100644
--- a/src/enc/lossless/losslessi_enc.h
+++ b/src/enc/lossless/losslessi_enc.h
@@ -139,6 +139,9 @@
TransformHeader::CCTransform third_transform,
WP2::ANSEnc* enc);
WP2Status ApplyColorIndexing(const CrunchConfig& config);
+ WP2Status ApplyNormalizeChannels(const std::array<int32_t, 4>& minima_range,
+ const std::array<int32_t, 4>& maxima_range,
+ TransformHeader& header, WP2::ANSEnc& enc);
// Copy the original pic_ when need.
WP2Status MakeInputImageCopy(const CrunchConfig& config);
diff --git a/src/wp2/format_constants.h b/src/wp2/format_constants.h
index 02d5882..4b08c90 100644
--- a/src/wp2/format_constants.h
+++ b/src/wp2/format_constants.h
@@ -372,6 +372,7 @@
kYCoCgR, // reversible YCoCg (unused)
kSubtractGreen,
kColorIndexing,
+ kNormalizeChannels, // maps the channels to a range of [0, num_colors-1]
kNum
};
// Possible image transform combinations that can be applied.
@@ -411,6 +412,9 @@
{TransformType::kColorIndexing, TransformType::kPredictor,
TransformType::kNum}},
{EncodingAlgorithm::kWebP,
+ {TransformType::kNormalizeChannels, TransformType::kPredictor,
+ TransformType::kNum}},
+ {EncodingAlgorithm::kWebP,
{TransformType::kPredictor, TransformType::kCrossColor,
TransformType::kNum}},
// Triplet.
diff --git a/tests/lossless/test_transforms.cc b/tests/lossless/test_transforms.cc
index a7553ef..cd208f5 100644
--- a/tests/lossless/test_transforms.cc
+++ b/tests/lossless/test_transforms.cc
@@ -127,4 +127,48 @@
WP2_STATUS_INVALID_PARAMETER);
}
+// Tests the cross color global transform.
+TEST(PerChannelIndexingTest, Pattern) {
+ constexpr char kSource1_64x48[] = "source1_64x48.png";
+ ArgbBuffer full_original(WP2_ARGB_32);
+ ASSERT_WP2_OK(ReadImage(testutil::GetTestDataPath(kSource1_64x48).c_str(),
+ &full_original));
+ for (int test_case = 0; test_case < 2; ++test_case) {
+ ArgbBuffer original(WP2_ARGB_32);
+ if (test_case == 0) {
+ WP2_ASSERT_STATUS(original.SetView(full_original));
+ } else {
+ // Trigger the non-palette code.
+ WP2_ASSERT_STATUS(
+ original.SetView(full_original, WP2::Rectangle(0, 0, 10, 10)));
+ }
+
+ // Define the encoder config for lossless, including kCrossColorGlobal.
+ EncoderConfig encoder_config;
+ encoder_config.alpha_quality = 100;
+ encoder_config.quality = 100;
+ encoder_config.keep_unmultiplied = true;
+ encoder_config.lossless_config =
+ std::make_shared<WP2::EncoderConfig::LosslessCrunchConfig>();
+ WP2L::CrunchConfig &config = *encoder_config.lossless_config;
+ config.algorithm = WP2L::EncodingAlgorithm::kWebP;
+ config.transform_bits = config.histo_bits = 5;
+ config.lz77s_types_to_try[0] = WP2L::kLZ77Standard;
+ config.lz77s_types_to_try_size = 1;
+ config.transforms[0].type = WP2L::TransformType::kNormalizeChannels;
+ config.transforms[1].type = WP2L::TransformType::kPredictor;
+ config.transforms[2].type = WP2L::TransformType::kNum;
+
+ MemoryWriter memory_writer;
+ WP2_ASSERT_STATUS(Encode(original, &memory_writer, encoder_config));
+
+ DecoderConfig decoder_config;
+ ArgbBuffer decompressed(WP2_ARGB_32);
+ WP2_ASSERT_STATUS(Decode(memory_writer.mem_, memory_writer.size_,
+ &decompressed, decoder_config));
+
+ EXPECT_TRUE(testutil::Compare(original, decompressed,
+ /*file_name=*/"cross_color_global"));
+ }
+}
} // namespace WP2