From 8b275092e0e15097e15d08548b509f42e2259bfa Mon Sep 17 00:00:00 2001 From: Aras Pranckevicius Date: Wed, 9 Oct 2024 12:34:49 +0200 Subject: [PATCH] Image: Add Quality setting to EXR DWAA/DWAB compression EXR DWAA and DWAB are conceptually similar to lossy JPG compression, with a tunable file size vs image quality parameter. However, previously Blender always used the fixed default setting, which is kinda similar to very high quality (like 97) for JPG. Internally EXR DWA/DWB quality parameter is inverted scale, i.e. 0 is best/lossless quality, and increased setting value means decreased quality. However the rest of Blender UI uses 1-100 JPG-like quality scale, where values above 90 are "visually lossless", 100 is lossless, and going below something like 50 would be visually quite lossy. So map that to internal DWA setting: - blender 100 -> DWA 0 - blender 97 -> DWA 45 The rest is linear relation based on those two points. Pull Request: https://projects.blender.org/blender/blender/pulls/128790 --- .../blender/blenkernel/intern/image_format.cc | 2 ++ .../blender/blenkernel/intern/image_save.cc | 5 ++-- .../editors/space_image/image_buttons.cc | 3 +++ source/blender/imbuf/IMB_openexr.hh | 1 + .../imbuf/intern/openexr/openexr_api.cpp | 27 ++++++++++++++++--- tests/python/bl_imbuf_save.py | 19 ++++++------- 6 files changed, 42 insertions(+), 15 deletions(-) diff --git a/source/blender/blenkernel/intern/image_format.cc b/source/blender/blenkernel/intern/image_format.cc index d489fb0fc13..3a888b8289c 100644 --- a/source/blender/blenkernel/intern/image_format.cc +++ b/source/blender/blenkernel/intern/image_format.cc @@ -118,6 +118,7 @@ int BKE_imtype_to_ftype(const char imtype, ImbFormatOptions *r_options) return IMB_FTYPE_TIF; } if (ELEM(imtype, R_IMF_IMTYPE_OPENEXR, R_IMF_IMTYPE_MULTILAYER)) { + r_options->quality = 90; return IMB_FTYPE_OPENEXR; } #ifdef WITH_CINEON @@ -649,6 +650,7 @@ void BKE_image_format_to_imbuf(ImBuf *ibuf, const ImageFormatData *imf) ibuf->foptions.flag |= OPENEXR_HALF; } ibuf->foptions.flag |= (imf->exr_codec & OPENEXR_COMPRESS); + ibuf->foptions.quality = quality; } #endif #ifdef WITH_CINEON diff --git a/source/blender/blenkernel/intern/image_save.cc b/source/blender/blenkernel/intern/image_save.cc index 9a273484dfd..6e00785ae7e 100644 --- a/source/blender/blenkernel/intern/image_save.cc +++ b/source/blender/blenkernel/intern/image_save.cc @@ -998,9 +998,10 @@ bool BKE_image_render_write_exr(ReportList *reports, BLI_file_ensure_parent_dir_exists(filepath); - int compress = (imf ? imf->exr_codec : 0); + const int compress = (imf ? imf->exr_codec : 0); + const int quality = (imf ? imf->quality : 90); bool success = IMB_exr_begin_write( - exrhandle, filepath, rr->rectx, rr->recty, compress, rr->stamp_data); + exrhandle, filepath, rr->rectx, rr->recty, compress, quality, rr->stamp_data); if (success) { IMB_exr_write_channels(exrhandle); } diff --git a/source/blender/editors/space_image/image_buttons.cc b/source/blender/editors/space_image/image_buttons.cc index d93176a6176..ba57bb9fed8 100644 --- a/source/blender/editors/space_image/image_buttons.cc +++ b/source/blender/editors/space_image/image_buttons.cc @@ -986,6 +986,9 @@ void uiTemplateImageSettings(uiLayout *layout, PointerRNA *imfptr, bool color_ma if (ELEM(imf->imtype, R_IMF_IMTYPE_OPENEXR, R_IMF_IMTYPE_MULTILAYER)) { uiItemR(col, imfptr, "exr_codec", UI_ITEM_NONE, nullptr, ICON_NONE); + if (ELEM(imf->exr_codec & OPENEXR_COMPRESS, R_IMF_EXR_CODEC_DWAA, R_IMF_EXR_CODEC_DWAB)) { + uiItemR(col, imfptr, "quality", UI_ITEM_NONE, nullptr, ICON_NONE); + } } if (is_render_out && ELEM(imf->imtype, R_IMF_IMTYPE_OPENEXR, R_IMF_IMTYPE_MULTILAYER)) { diff --git a/source/blender/imbuf/IMB_openexr.hh b/source/blender/imbuf/IMB_openexr.hh index d93ed3d0e25..501ca41b913 100644 --- a/source/blender/imbuf/IMB_openexr.hh +++ b/source/blender/imbuf/IMB_openexr.hh @@ -50,6 +50,7 @@ bool IMB_exr_begin_write(void *handle, int width, int height, int compress, + int quality, const StampData *stamp); /** * Only used for writing temp. render results (not image files) diff --git a/source/blender/imbuf/intern/openexr/openexr_api.cpp b/source/blender/imbuf/intern/openexr/openexr_api.cpp index af7ed04d9ae..5668ca673ec 100644 --- a/source/blender/imbuf/intern/openexr/openexr_api.cpp +++ b/source/blender/imbuf/intern/openexr/openexr_api.cpp @@ -77,6 +77,7 @@ #include "BLI_blenlib.h" #include "BLI_fileops.h" +#include "BLI_math_base.hh" #include "BLI_math_color.h" #include "BLI_mmap.h" #include "BLI_threads.h" @@ -385,7 +386,20 @@ bool imb_is_a_openexr(const uchar *mem, const size_t size) return Imf::isImfMagic((const char *)mem); } -static void openexr_header_compression(Header *header, int compression) +static int openexr_jpg_like_quality_to_dwa_quality(int q) +{ + q = blender::math::clamp(q, 0, 100); + + /* Map "visually lossless" JPG quality of 97 to default DWA level of 45, + * "lossless" JPG quality of 100 to DWA level of 0, and everything else + * linearly based on those. */ + constexpr int x0 = 100, y0 = 0; + constexpr int x1 = 97, y1 = 45; + q = y0 + (q - x0) * (y1 - y0) / (x1 - x0); + return q; +} + +static void openexr_header_compression(Header *header, int compression, int quality) { switch (compression) { case R_IMF_EXR_CODEC_NONE: @@ -415,9 +429,11 @@ static void openexr_header_compression(Header *header, int compression) #if OPENEXR_VERSION_MAJOR > 2 || (OPENEXR_VERSION_MAJOR >= 2 && OPENEXR_VERSION_MINOR >= 2) case R_IMF_EXR_CODEC_DWAA: header->compression() = DWAA_COMPRESSION; + header->dwaCompressionLevel() = openexr_jpg_like_quality_to_dwa_quality(quality); break; case R_IMF_EXR_CODEC_DWAB: header->compression() = DWAB_COMPRESSION; + header->dwaCompressionLevel() = openexr_jpg_like_quality_to_dwa_quality(quality); break; #endif default: @@ -462,7 +478,8 @@ static bool imb_save_openexr_half(ImBuf *ibuf, const char *filepath, const int f try { Header header(width, height); - openexr_header_compression(&header, ibuf->foptions.flag & OPENEXR_COMPRESS); + openexr_header_compression( + &header, ibuf->foptions.flag & OPENEXR_COMPRESS, ibuf->foptions.quality); openexr_header_metadata(&header, ibuf); /* create channels */ @@ -563,7 +580,8 @@ static bool imb_save_openexr_float(ImBuf *ibuf, const char *filepath, const int try { Header header(width, height); - openexr_header_compression(&header, ibuf->foptions.flag & OPENEXR_COMPRESS); + openexr_header_compression( + &header, ibuf->foptions.flag & OPENEXR_COMPRESS, ibuf->foptions.quality); openexr_header_metadata(&header, ibuf); /* create channels */ @@ -875,6 +893,7 @@ bool IMB_exr_begin_write(void *handle, int width, int height, int compress, + int quality, const StampData *stamp) { ExrHandle *data = (ExrHandle *)handle; @@ -889,7 +908,7 @@ bool IMB_exr_begin_write(void *handle, header.channels().insert(echan->name, Channel(echan->use_half_float ? Imf::HALF : Imf::FLOAT)); } - openexr_header_compression(&header, compress); + openexr_header_compression(&header, compress, quality); BKE_stamp_info_callback( &header, const_cast(stamp), openexr_header_metadata_callback, false); /* header.lineOrder() = DECREASING_Y; this crashes in windows for file read! */ diff --git a/tests/python/bl_imbuf_save.py b/tests/python/bl_imbuf_save.py index 8e4c8cefe23..deafbbfbf7e 100644 --- a/tests/python/bl_imbuf_save.py +++ b/tests/python/bl_imbuf_save.py @@ -45,7 +45,8 @@ class ImBufTest(AbstractImBufTest): for s in settings: if s == "color_depth": name += str(settings[s]).rjust(2, '0') + "-" - else: + # do not embed exr quality into test file name + elif not(s == "quality" and ext == "exr"): name += str(settings[s]) + "-" setattr(image_settings, s, settings[s]) @@ -123,17 +124,17 @@ class ImBufSaveTest(ImBufTest): self.skip_if_format_missing("OPENEXR") self.check(src="rgba08", ext="exr", settings={"file_format": "OPEN_EXR", "color_mode": "BW", "color_depth": "16", "exr_codec": "ZIP"}) - self.check(src="rgba08", ext="exr", settings={"file_format": "OPEN_EXR", "color_mode": "RGB", "color_depth": "16", "exr_codec": "DWAA"}) - self.check(src="rgba08", ext="exr", settings={"file_format": "OPEN_EXR", "color_mode": "RGBA", "color_depth": "16", "exr_codec": "DWAB"}) - self.check(src="rgba08", ext="exr", settings={"file_format": "OPEN_EXR", "color_mode": "BW", "color_depth": "32", "exr_codec": "DWAB"}) - self.check(src="rgba08", ext="exr", settings={"file_format": "OPEN_EXR", "color_mode": "RGB", "color_depth": "32", "exr_codec": "DWAA"}) + self.check(src="rgba08", ext="exr", settings={"file_format": "OPEN_EXR", "color_mode": "RGB", "color_depth": "16", "exr_codec": "DWAA", "quality": 97}) + self.check(src="rgba08", ext="exr", settings={"file_format": "OPEN_EXR", "color_mode": "RGBA", "color_depth": "16", "exr_codec": "DWAB", "quality": 97}) + self.check(src="rgba08", ext="exr", settings={"file_format": "OPEN_EXR", "color_mode": "BW", "color_depth": "32", "exr_codec": "DWAB", "quality": 97}) + self.check(src="rgba08", ext="exr", settings={"file_format": "OPEN_EXR", "color_mode": "RGB", "color_depth": "32", "exr_codec": "DWAA", "quality": 97}) self.check(src="rgba08", ext="exr", settings={"file_format": "OPEN_EXR", "color_mode": "RGBA", "color_depth": "32", "exr_codec": "ZIP"}) self.check(src="rgba32", ext="exr", settings={"file_format": "OPEN_EXR", "color_mode": "BW", "color_depth": "16", "exr_codec": "ZIP"}) - self.check(src="rgba32", ext="exr", settings={"file_format": "OPEN_EXR", "color_mode": "RGB", "color_depth": "16", "exr_codec": "DWAA"}) - self.check(src="rgba32", ext="exr", settings={"file_format": "OPEN_EXR", "color_mode": "RGBA", "color_depth": "16", "exr_codec": "DWAB"}) - self.check(src="rgba32", ext="exr", settings={"file_format": "OPEN_EXR", "color_mode": "BW", "color_depth": "32", "exr_codec": "DWAB"}) - self.check(src="rgba32", ext="exr", settings={"file_format": "OPEN_EXR", "color_mode": "RGB", "color_depth": "32", "exr_codec": "DWAA"}) + self.check(src="rgba32", ext="exr", settings={"file_format": "OPEN_EXR", "color_mode": "RGB", "color_depth": "16", "exr_codec": "DWAA", "quality": 97}) + self.check(src="rgba32", ext="exr", settings={"file_format": "OPEN_EXR", "color_mode": "RGBA", "color_depth": "16", "exr_codec": "DWAB", "quality": 97}) + self.check(src="rgba32", ext="exr", settings={"file_format": "OPEN_EXR", "color_mode": "BW", "color_depth": "32", "exr_codec": "DWAB", "quality": 97}) + self.check(src="rgba32", ext="exr", settings={"file_format": "OPEN_EXR", "color_mode": "RGB", "color_depth": "32", "exr_codec": "DWAA", "quality": 97}) self.check(src="rgba32", ext="exr", settings={"file_format": "OPEN_EXR", "color_mode": "RGBA", "color_depth": "32", "exr_codec": "ZIP"}) def test_save_hdr(self):