Skip to content

Commit

Permalink
Support encoding 16-bit images (#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
y-guyon authored Jan 3, 2025
1 parent 82dcdc1 commit 5089ab4
Show file tree
Hide file tree
Showing 13 changed files with 103 additions and 31 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## v0.4.1

- Bump the version of libwebp2 in deps.sh.
- Support encoding 16-bit images with JPEG XL. Encode 16-bit images as twice as
wide 8-bit images with other codecs.

## v0.4.0

- Bump the version of libwebp2 in deps.sh.
Expand Down
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ cmake_minimum_required(VERSION 3.20)
project(
codec-compare-gen
LANGUAGES CXX
VERSION 0.4.0)
VERSION 0.4.1)
set(CMAKE_CXX_STANDARD 17)

option(BUILD_SHARED_LIBS "Build the shared codec-compare-gen library" ON)
Expand Down
2 changes: 1 addition & 1 deletion deps.sh
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ pushd third_party

git clone https://chromium.googlesource.com/codecs/libwebp2
pushd libwebp2
git checkout b03ac3800bf6a4cfd45e1302770f385eb08b39c7
git checkout 169f4159a465b7b4241c0d60ae7f37b15a9b2d65
cmake -S . -B build \
-DCMAKE_PREFIX_PATH="../libwebp/src/;../libwebp/build/" \
-DWP2_BUILD_TESTS=OFF \
Expand Down
17 changes: 14 additions & 3 deletions src/codec.cc
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ bool CodecIsSupportedByBrowsers(Codec codec) {

namespace {

// Returns the format layout required by the API of the given codec.
// Returns the 8-bit format layout required by the API of the given codec.
WP2SampleFormat CodecToNeededFormat(Codec codec, bool has_transparency) {
if (codec == Codec::kWebp) {
return WebPPictureFormat();
Expand All @@ -159,7 +159,6 @@ WP2SampleFormat CodecToNeededFormat(Codec codec, bool has_transparency) {
if (codec == Codec::kJpegturbo || codec == Codec::kJpegli ||
codec == Codec::kJpegsimple || codec == Codec::kJpegmoz) {
return WP2_RGB_24;
return WP2_ARGB_32;
}
// Other formats support this layout even for opaque images.
return WP2_ARGB_32;
Expand All @@ -184,12 +183,23 @@ StatusOr<TaskOutput> EncodeDecode(const TaskInput& input,
for (const Frame& frame : original_image) {
has_transparency |= frame.pixels.HasTransparency();
}
const WP2SampleFormat needed_format =
WP2SampleFormat needed_format =
CodecToNeededFormat(input.codec_settings.codec, has_transparency);
if (initial_format != needed_format) {
needed_format = WP2FormatAtbpc(
needed_format, WP2Formatbpc(original_image.front().pixels.format()));
CHECK_OR_RETURN(needed_format != WP2_FORMAT_NUM, quiet);
// Ditch alpha because the image is opaque.
ASSIGN_OR_RETURN(original_image,
CloneAs(original_image, needed_format, quiet));
}
if (WP2Formatbpc(original_image.front().pixels.format()) == 16 &&
input.codec_settings.codec != Codec::kJpegXl &&
input.codec_settings.quality == kQualityLossless) {
// The codec does not support 16-bit images. Consider the frames to be 8-bit
// and twice as large. The compression rate is likely terrible.
ASSIGN_OR_RETURN(original_image, SpreadTo8bit(original_image, quiet));
}

auto encode_func =
input.codec_settings.codec == Codec::kWebp ? &EncodeWebp
Expand Down Expand Up @@ -238,6 +248,7 @@ StatusOr<TaskOutput> EncodeDecode(const TaskInput& input,
task.encoding_duration = encoding_duration.seconds();
task.image_width = original_image.front().pixels.width();
task.image_height = original_image.front().pixels.height();
task.bit_depth = WP2Formatbpc(original_image.front().pixels.format());
task.num_frames = static_cast<uint32_t>(original_image.size());
task.encoded_size = encoded_image.size;

Expand Down
9 changes: 7 additions & 2 deletions src/codec_jpegxl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,9 @@ StatusOr<WP2::Data> EncodeJxl(const TaskInput& input,
quiet);

CHECK_OR_RETURN(frame.pixels.format() == WP2_RGBA_32 ||
frame.pixels.format() == WP2_RGB_24,
frame.pixels.format() == WP2_RGB_24 ||
frame.pixels.format() == WP2_RGBA_64 ||
frame.pixels.format() == WP2_RGB_48,
quiet)
<< "libjxl requires RGB(A)";
const JxlPixelFormat pixel_format =
Expand Down Expand Up @@ -252,7 +254,10 @@ StatusOr<std::pair<Image, double>> DecodeJxl(const TaskInput& input,
info.animation.tps_denominator == 1000,
quiet);
}
const WP2SampleFormat format = info.alpha_bits > 0 ? WP2_RGBA_32 : WP2_RGB_24;
const WP2SampleFormat format =
info.bits_per_sample == 8
? (info.alpha_bits > 0 ? WP2_RGBA_32 : WP2_RGB_24)
: (info.alpha_bits > 0 ? WP2_RGBA_64 : WP2_RGB_48);

Image image;
while ((status = JxlDecoderProcessInput(decoder.get())) ==
Expand Down
28 changes: 27 additions & 1 deletion src/frame.cc
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,31 @@ StatusOr<Image> CloneAs(const Image& from, WP2SampleFormat format, bool quiet) {
to.emplace_back(WP2::ArgbBuffer(format), frame.duration_ms);
CHECK_OR_RETURN(to.back().pixels.ConvertFrom(frame.pixels) == WP2_STATUS_OK,
quiet);
// Check that there was no bit depth loss.
CHECK_OR_RETURN(WP2Formatbpc(to.back().pixels.format()) ==
WP2Formatbpc(frame.pixels.format()),
quiet);
}
return to;
}

StatusOr<Image> SpreadTo8bit(const Image& from, bool quiet) {
Image to;
to.reserve(from.size());
for (const Frame& frame : from) {
const WP2SampleFormat format = WP2FormatAtbpc(frame.pixels.format(), 8);
CHECK_OR_RETURN(format != WP2_FORMAT_NUM, quiet);
to.emplace_back(WP2::ArgbBuffer(format), frame.duration_ms);
CHECK_OR_RETURN(
to.back().pixels.Import(
format,
frame.pixels.width() *
(WP2FormatBpp(frame.pixels.format()) /
WP2FormatNumChannels(frame.pixels.format())),
frame.pixels.height(),
reinterpret_cast<const uint8_t*>(frame.pixels.GetRow(0)),
frame.pixels.stride()) == WP2_STATUS_OK,
quiet);
}
return to;
}
Expand Down Expand Up @@ -91,7 +116,8 @@ StatusOr<Image> ReadStillImageOrAnimation(const char* file_path,
<< file_path << " was ignored" << std::endl;
continue;
}

format = WP2FormatAtbpc(format, WP2Formatbpc(buffer.format()));
CHECK_OR_RETURN(format != WP2_FORMAT_NUM, quiet);
WP2::ArgbBuffer pixels(format);
// All metadata is discarded during the conversion.
CHECK_OR_RETURN(pixels.ConvertFrom(buffer) == WP2_STATUS_OK, quiet);
Expand Down
1 change: 1 addition & 0 deletions src/frame.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ uint32_t GetDurationMs(const Image& image);
// Makes a deep copy of the given frame sequence and converts the pixels to the
// given format.
StatusOr<Image> CloneAs(const Image& from, WP2SampleFormat format, bool quiet);
StatusOr<Image> SpreadTo8bit(const Image& from, bool quiet);

// Makes a shallow copy of the given frame sequence.
StatusOr<Image> MakeView(const Image& from, bool quiet);
Expand Down
4 changes: 2 additions & 2 deletions src/framework.cc
Original file line number Diff line number Diff line change
Expand Up @@ -420,8 +420,8 @@ Status Compare(const std::vector<std::string>& image_paths,
<< " Quality: " << codec_settings.quality << std::endl
<< " Original file path: " << input.image_path << std::endl
<< " Image dimensions: " << task.image_width << "x"
<< task.image_height << " (" << task.num_frames << " frames)"
<< std::endl
<< task.image_height << " (" << task.num_frames << " "
<< task.bit_depth << "-bit frames)" << std::endl
<< " Encoded file path: " << input.encoded_path << std::endl;
std::cout << "Output stats" << std::endl
<< " Encoded size: " << task.encoded_size << std::endl
Expand Down
10 changes: 6 additions & 4 deletions src/result_json.cc
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ Status TasksToJson(const std::string& batch_name, CodecSettings settings,
const std::string image_prefix =
GetImagePathCommonPrefix(tasks, /*get_encoded_path=*/false);
const std::string build_cmd =
"git clone -b v0.4.0 --depth 1"
"git clone -b v0.4.1 --depth 1"
" https://github.com/webmproject/codec-compare-gen.git &&"
" cd codec-compare-gen && ./deps.sh &&"
" cmake -S . -B build -DCMAKE_CXX_COMPILER=clang++ &&"
Expand Down Expand Up @@ -170,9 +170,10 @@ Status TasksToJson(const std::string& batch_name, CodecSettings settings,
file << R"json(
"field_descriptions": [
{"original_name": "Original image file name"},
{"width": "Pixel columns in the original image"},
{"height": "Pixel rows in the original image"},
{"frame_count": "Number of frames in the original image"},)json";
{"width": "Pixel columns in the image that was encoded"},
{"height": "Pixel rows in the image that was encoded"},
{"depth": "Bit depth of the image that was encoded"},
{"frame_count": "Number of frames in the image that was encoded"},)json";
if (!lossless) {
file << R"json(
{"chroma_subsampling": "Compression chroma subsampling parameter"},)json";
Expand Down Expand Up @@ -216,6 +217,7 @@ Status TasksToJson(const std::string& batch_name, CodecSettings settings,
<< ",";
file << task.image_width << ",";
file << task.image_height << ",";
file << task.bit_depth << ",";
file << task.num_frames << ",";
if (!lossless) {
file << SubsamplingToString(
Expand Down
12 changes: 7 additions & 5 deletions src/task.cc
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ std::string TaskOutput::Serialize() const {
<< ", " << task_input.codec_settings.effort << ", "
<< task_input.codec_settings.quality << ", "
<< Escape(task_input.image_path) << ", " << image_width << ", "
<< image_height << ", " << num_frames << ", "
<< image_height << ", " << bit_depth << ", " << num_frames << ", "
<< Escape(task_input.encoded_path) << ", " << encoded_size << ", "
<< encoding_duration << ", " << decoding_duration << ", "
<< decoding_color_conversion_duration;
Expand All @@ -98,7 +98,7 @@ std::string TaskOutput::Serialize() const {

namespace {

constexpr size_t kNumNonDistortionTokens = 13;
constexpr size_t kNumNonDistortionTokens = 14;

StatusOr<TaskOutput> UnserializeNoDistortion(
const std::string& serialized_task, const std::vector<std::string> tokens,
Expand Down Expand Up @@ -130,6 +130,7 @@ StatusOr<TaskOutput> UnserializeNoDistortion(
ASSIGN_OR_RETURN(task.task_input.image_path, Unescape(tokens[t++], quiet));
task.image_width = std::stoul(tokens[t++]);
task.image_height = std::stoul(tokens[t++]);
task.bit_depth = std::stoul(tokens[t++]);
task.num_frames = std::stoul(tokens[t++]);

ASSIGN_OR_RETURN(task.task_input.encoded_path, Unescape(tokens[t++], quiet));
Expand All @@ -140,9 +141,9 @@ StatusOr<TaskOutput> UnserializeNoDistortion(

CHECK_OR_RETURN(t == kNumNonDistortionTokens, quiet);

CHECK_OR_RETURN(
task.image_width > 0 && task.image_height > 0 && task.num_frames > 0,
quiet)
CHECK_OR_RETURN(task.image_width > 0 && task.image_height > 0 &&
task.bit_depth > 0 && task.num_frames > 0,
quiet)
<< "Bad image dimensions in \"" << serialized_task << "\"";
CHECK_OR_RETURN(task.encoded_size > 0, quiet)
<< "Bad encoded size in \"" << serialized_task << "\"";
Expand Down Expand Up @@ -235,6 +236,7 @@ bool TaskOutputsAreRepetitions(const TaskOutput& a, const TaskOutput& b) {
if (!(a.task_input == b.task_input)) return false;
if (a.image_width != b.image_width) return false;
if (a.image_height != b.image_height) return false;
if (a.bit_depth != b.bit_depth) return false;
if (a.num_frames != b.num_frames) return false;
if (a.encoded_size != b.encoded_size) return false;
for (size_t metric = 0; metric < kNumDistortionMetrics; ++metric) {
Expand Down
1 change: 1 addition & 0 deletions src/task.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ struct TaskOutput {

uint32_t image_width; // in pixels
uint32_t image_height; // in pixels
uint32_t bit_depth; // per sample
uint32_t num_frames;
size_t encoded_size; // in bytes
double encoding_duration; // in seconds
Expand Down
16 changes: 15 additions & 1 deletion tests/test_framework.cc
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,21 @@ TEST_F(FrameworkTest, AllChromaSubsamplings) {
TEST_F(FrameworkTest, AllCodecsSupporting16bits) {
ComparisonSettings settings;
settings.codec_settings.push_back(
{Codec::kJpegXl, Subsampling::kDefault, /*effort=*/1, /*quality=*/100});
{Codec::kJpegXl, Subsampling::kDefault, /*effort=*/1, kQualityLossless});
EXPECT_EQ(Compare({std::string(data_path) + "alpha31x32_16bits.png",
std::string(data_path) + "gradient32x32_16bits.png"},
settings, TempPath("completed_tasks.csv"), TempPath()),
Status::kOk);
}

TEST_F(FrameworkTest, AllCodecsCompressing16bitsAsTwiceAsWide8bits) {
ComparisonSettings settings;
settings.codec_settings.push_back(
{Codec::kWebp, Subsampling::kDefault, /*effort=*/0, kQualityLossless});
settings.codec_settings.push_back(
{Codec::kWebp2, Subsampling::kDefault, /*effort=*/0, kQualityLossless});
settings.codec_settings.push_back(
{Codec::kAvif, Subsampling::kDefault, /*speed*/ 9, kQualityLossless});
EXPECT_EQ(Compare({std::string(data_path) + "alpha31x32_16bits.png",
std::string(data_path) + "gradient32x32_16bits.png"},
settings, TempPath("completed_tasks.csv"), TempPath()),
Expand Down
26 changes: 15 additions & 11 deletions tests/test_task.cc
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ void ExpectEq(const std::vector<std::vector<TaskOutput>>& actual,
EXPECT_EQ(actual[i][j].task_input, expected[i][j].task_input);
EXPECT_EQ(actual[i][j].image_width, expected[i][j].image_width);
EXPECT_EQ(actual[i][j].image_height, expected[i][j].image_height);
EXPECT_EQ(actual[i][j].bit_depth, expected[i][j].bit_depth);
EXPECT_EQ(actual[i][j].num_frames, expected[i][j].num_frames);
EXPECT_EQ(actual[i][j].encoded_size, expected[i][j].encoded_size);
EXPECT_EQ(actual[i][j].encoding_duration,
Expand All @@ -55,7 +56,7 @@ constexpr Subsampling kDef = Subsampling::kDefault;

TEST(SplitByCodecSettingsAndAggregateByImageTest, Simple) {
const std::vector<TaskOutput> results = {
{{{kWebp, kDef, /*effort=*/0, /*quality=*/0}, "img"}, 1, 2, 3, 0}};
{{{kWebp, kDef, /*effort=*/0, /*quality=*/0}, "img"}, 1, 2, 8, 3, 0}};
const auto aggregate = SplitByCodecSettingsAndAggregateByImageAndQuality(
results, /*quiet=*/false);
ASSERT_EQ(aggregate.status, Status::kOk);
Expand All @@ -64,15 +65,16 @@ TEST(SplitByCodecSettingsAndAggregateByImageTest, Simple) {

TEST(SplitByCodecSettingsAndAggregateByImageTest, Multiple) {
const std::vector<TaskInput> single_inputs = {
{{kWebp, kDef, /*effort=*/0, /*quality=*/0}, "imgA"},
{{kWebp, kDef, /*effort=*/0, /*quality=*/0}, "imgB"},
{{kWebp, kDef, /*effort=*/1, /*quality=*/0}, "imgA"},
{{kWebp, kDef, /*effort=*/0, /*quality=*/100}, "imgA"},
{{kWebp2, kDef, /*effort=*/0, /*quality=*/0}, "imgA"}};
{{kWebp, kDef, /*effort=*/0, /*quality=*/0}, "A"},
{{kWebp, kDef, /*effort=*/0, /*quality=*/0}, "B"},
{{kWebp, kDef, /*effort=*/1, /*quality=*/0}, "A"},
{{kWebp, kDef, /*effort=*/0, /*quality=*/100}, "A"},
{{kWebp2, kDef, /*effort=*/0, /*quality=*/0}, "A"}};
std::vector<TaskOutput> results;
results.reserve(single_inputs.size() * 2);
constexpr uint32_t kImageWidth = 8;
constexpr uint32_t kImageHeight = 9;
constexpr uint32_t kImageDepth = 8;
constexpr uint32_t kNumFrames = 1;
size_t encoded_size = 1;
double encoding_duration = 1;
Expand All @@ -85,6 +87,7 @@ TEST(SplitByCodecSettingsAndAggregateByImageTest, Multiple) {
results.push_back({input,
kImageWidth,
kImageHeight,
kImageDepth,
kNumFrames,
encoded_size,
encoding_duration++,
Expand All @@ -94,6 +97,7 @@ TEST(SplitByCodecSettingsAndAggregateByImageTest, Multiple) {
results.push_back({input,
kImageWidth,
kImageHeight,
kImageDepth,
kNumFrames,
encoded_size++,
encoding_duration++,
Expand All @@ -110,11 +114,11 @@ TEST(SplitByCodecSettingsAndAggregateByImageTest, Multiple) {
ASSERT_EQ(aggregate.status, Status::kOk);
ExpectEq(
aggregate.value,
{{{{{kWebp, kDef, 0, 0}, "imgA"}, 8, 9, 1, 1u, 1.5, 1.5, 0.5, {20.0}},
{{{kWebp, kDef, 0, 100}, "imgA"}, 8, 9, 1, 4u, 7.5, 7.5, 3.5, {23.0}},
{{{kWebp, kDef, 0, 0}, "imgB"}, 8, 9, 1, 2u, 3.5, 3.5, 1.5, {21.0}}},
{{{{kWebp, kDef, 1, 0}, "imgA"}, 8, 9, 1, 3u, 5.5, 5.5, 2.5, {22.0}}},
{{{{kWebp2, kDef, 0, 0}, "imgA"}, 8, 9, 1, 5u, 9.5, 9.5, 4.5, {24.0}}}});
{{{{{kWebp, kDef, 0, 0}, "A"}, 8, 9, 8, 1, 1u, 1.5, 1.5, 0.5, {20.0}},
{{{kWebp, kDef, 0, 100}, "A"}, 8, 9, 8, 1, 4u, 7.5, 7.5, 3.5, {23.0}},
{{{kWebp, kDef, 0, 0}, "B"}, 8, 9, 8, 1, 2u, 3.5, 3.5, 1.5, {21.0}}},
{{{{kWebp, kDef, 1, 0}, "A"}, 8, 9, 8, 1, 3u, 5.5, 5.5, 2.5, {22.0}}},
{{{{kWebp2, kDef, 0, 0}, "A"}, 8, 9, 8, 1, 5u, 9.5, 9.5, 4.5, {24.0}}}});
}

} // namespace
Expand Down

0 comments on commit 5089ab4

Please sign in to comment.