mirror of
https://github.com/blender/blender
synced 2026-09-29 04:37:17 +03:00
Fix #152455: OpenEXR DWAA/DWAB saving leads to inf values
DWAA/DWAB lossy compression uses DCT-based encoding, where values near HALF_MAX can overshoot to infinity after decompression, due to DCT ringing. There are different ways to resolves it and it's not so clear what the ideal solution would be and how it would be handled upstream. By default it will already convert values > HALF_MAX to inf and it's up to the application to clamp if they want to (like we do). As a workaround, clamp half float values before writing with an empirically determined value that takes into account that lower quality makes this problem worse. The resulting files should then decompress without inf in existing software. Pull Request: https://projects.blender.org/blender/blender/pulls/154061
This commit is contained in:
parent
c17eb901f7
commit
fac8b300e0
15 changed files with 78 additions and 14 deletions
|
|
@ -386,9 +386,9 @@ struct _RGBAZ {
|
|||
|
||||
using RGBAZ = _RGBAZ;
|
||||
|
||||
static half float_to_half_safe(const float value)
|
||||
static half float_to_half_safe(const float value, const float max_val = HALF_MAX)
|
||||
{
|
||||
return half(clamp_f(value, -HALF_MAX, HALF_MAX));
|
||||
return half(clamp_f(value, -max_val, max_val));
|
||||
}
|
||||
|
||||
bool imb_is_a_openexr(const uchar *mem, const size_t size)
|
||||
|
|
@ -400,7 +400,7 @@ bool imb_is_a_openexr(const uchar *mem, const size_t size)
|
|||
return Imf::isImfMagic((const char *)mem);
|
||||
}
|
||||
|
||||
static int openexr_jpg_like_quality_to_dwa_quality(int q)
|
||||
static int openexr_jpg_like_quality_to_dwa_compression_level(int q)
|
||||
{
|
||||
q = math::clamp(q, 0, 100);
|
||||
|
||||
|
|
@ -413,6 +413,24 @@ static int openexr_jpg_like_quality_to_dwa_quality(int q)
|
|||
return q;
|
||||
}
|
||||
|
||||
static float compression_half_max(const int compression, const int quality)
|
||||
{
|
||||
if (ELEM(compression, R_IMF_EXR_CODEC_DWAA, R_IMF_EXR_CODEC_DWAB)) {
|
||||
/* Empirically determined margin to prevent DWAA/DWAB lossy compression
|
||||
* from overshooting to infinity. The DWA compression uses log2-luminance
|
||||
* DCT, and overshoot increases with the compression level.
|
||||
*
|
||||
* Tested with randomized pixel patterns across various compression levels
|
||||
* to find a tight bound. */
|
||||
const int level = openexr_jpg_like_quality_to_dwa_compression_level(quality);
|
||||
const float margin = (level <= 50) ? (2048.0f + 32.0f * level) :
|
||||
(3624.0f + 128.0f * (level - 50));
|
||||
return HALF_MAX - margin;
|
||||
}
|
||||
|
||||
return HALF_MAX;
|
||||
}
|
||||
|
||||
static void openexr_header_compression(Header *header, int compression, int quality)
|
||||
{
|
||||
switch (compression) {
|
||||
|
|
@ -443,11 +461,11 @@ static void openexr_header_compression(Header *header, int compression, int qual
|
|||
#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);
|
||||
header->dwaCompressionLevel() = openexr_jpg_like_quality_to_dwa_compression_level(quality);
|
||||
break;
|
||||
case R_IMF_EXR_CODEC_DWAB:
|
||||
header->compression() = DWAB_COMPRESSION;
|
||||
header->dwaCompressionLevel() = openexr_jpg_like_quality_to_dwa_quality(quality);
|
||||
header->dwaCompressionLevel() = openexr_jpg_like_quality_to_dwa_compression_level(quality);
|
||||
break;
|
||||
#endif
|
||||
#if COMBINED_OPENEXR_VERSION >= 30400
|
||||
|
|
@ -583,11 +601,13 @@ 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_CODEC_MASK, ibuf->foptions.quality);
|
||||
const int compression = ibuf->foptions.flag & OPENEXR_CODEC_MASK;
|
||||
openexr_header_compression(&header, compression, ibuf->foptions.quality);
|
||||
openexr_header_metadata_global(&header, ibuf->metadata, ibuf->ppm);
|
||||
openexr_header_metadata_colorspace(&header, ibuf);
|
||||
|
||||
const float half_max_val = compression_half_max(compression, ibuf->foptions.quality);
|
||||
|
||||
/* create channels */
|
||||
header.channels().insert("R", Channel(HALF));
|
||||
header.channels().insert("G", Channel(HALF));
|
||||
|
|
@ -627,10 +647,10 @@ static bool imb_save_openexr_half(ImBuf *ibuf, const char *filepath, const int f
|
|||
from = ibuf->float_buffer.data + int64_t(channels) * i * width;
|
||||
|
||||
for (int j = ibuf->x; j > 0; j--) {
|
||||
to->r = float_to_half_safe(from[0]);
|
||||
to->g = float_to_half_safe((channels >= 2) ? from[1] : from[0]);
|
||||
to->b = float_to_half_safe((channels >= 3) ? from[2] : from[0]);
|
||||
to->a = float_to_half_safe((channels >= 4) ? from[3] : 1.0f);
|
||||
to->r = float_to_half_safe(from[0], half_max_val);
|
||||
to->g = float_to_half_safe((channels >= 2) ? from[1] : from[0], half_max_val);
|
||||
to->b = float_to_half_safe((channels >= 3) ? from[2] : from[0], half_max_val);
|
||||
to->a = float_to_half_safe((channels >= 4) ? from[3] : 1.0f, half_max_val);
|
||||
to++;
|
||||
from += channels;
|
||||
}
|
||||
|
|
@ -840,6 +860,7 @@ struct ExrHandle {
|
|||
|
||||
bool write_multipart = false;
|
||||
bool has_layer_pass_names = false;
|
||||
float half_max_val = HALF_MAX;
|
||||
|
||||
int tilex = 0, tiley = 0;
|
||||
int width = 0, height = 0;
|
||||
|
|
@ -1016,6 +1037,7 @@ bool IMB_exr_begin_write(ExrHandle *handle,
|
|||
handle->height = height;
|
||||
|
||||
openexr_header_compression(&header, compress, quality);
|
||||
handle->half_max_val = compression_half_max(compress, quality);
|
||||
|
||||
if (!handle->write_multipart) {
|
||||
/* If we're writing single part, we can only add one colorspace even if there are
|
||||
|
|
@ -1207,7 +1229,7 @@ void IMB_exr_write_channels(ExrHandle *handle)
|
|||
const float *rect = echan.rect;
|
||||
half *cur = current_rect_half;
|
||||
for (size_t i = 0; i < num_pixels; i++, cur++) {
|
||||
*cur = float_to_half_safe(rect[i * echan.xstride]);
|
||||
*cur = float_to_half_safe(rect[i * echan.xstride], handle->half_max_val);
|
||||
}
|
||||
half *rect_to_write = current_rect_half + (handle->height - 1L) * handle->width;
|
||||
frameBuffer.insert(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b64c4b4db47b51e2f7c69d38ba93f30e69cb80ee0761a37a00cbd18361874cce
|
||||
size 18359
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4800ee496a6d311b2aafb48cd9300caf43345afcfffdf5575f6a827bd2e5561d
|
||||
size 10616
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9605dc81541c513aabf1980635ba9edd91cf8c2a0becc0b6cec76ed7f910670b
|
||||
size 1735
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bdb609df4d64807ef380b4d3f169f519c7c7368e9af47d4ca3acf1e87062323c
|
||||
size 1844
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:62be1e70a478b0c1aecedcde396920e53a611bc88e0780a016160fb12b637502
|
||||
size 2261
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:24eca7e95e7ade8c4e7040fcdac9bc78fceb0f64e2b2f9bf4db4d073850e2188
|
||||
size 3691
|
||||
|
|
@ -0,0 +1 @@
|
|||
channels=4 is_float=True colorspace='Linear Rec.709' alpha_mode='PREMUL'
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:df5bfcb8ca2287dc43f2430212bd6f35653a2cd50d2bd61c0c70c0043ba3f0a0
|
||||
size 4262
|
||||
|
|
@ -0,0 +1 @@
|
|||
channels=4 is_float=True colorspace='Linear Rec.709' alpha_mode='PREMUL'
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f85eaa2e2fbbf38840373295d989aea2f29a11a14002215f63d69718685d65c0
|
||||
size 4335
|
||||
|
|
@ -0,0 +1 @@
|
|||
channels=4 is_float=True colorspace='Linear Rec.709' alpha_mode='PREMUL'
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:decd767856944419fae0b484d1f81bad691450cddddee3aaa1f25795e1f1ea6e
|
||||
size 4092
|
||||
|
|
@ -0,0 +1 @@
|
|||
channels=4 is_float=True colorspace='Linear Rec.709' alpha_mode='PREMUL'
|
||||
|
|
@ -18,6 +18,7 @@ args = None
|
|||
|
||||
TEMPLATE_RGBA08 = "template-rgba08.png"
|
||||
TEMPLATE_RGBA32 = "template-rgba32.exr"
|
||||
TEMPLATE_HIGH_VALUES = "extreme_values/template-high-values.exr"
|
||||
|
||||
|
||||
class ImBufTest(AbstractImBufTest):
|
||||
|
|
@ -31,7 +32,7 @@ class ImBufTest(AbstractImBufTest):
|
|||
def _load_template_image(self, name, template_name):
|
||||
image_path = str(self.test_dir.joinpath(template_name))
|
||||
bpy.ops.image.open(filepath=image_path)
|
||||
img = bpy.data.images[template_name]
|
||||
img = bpy.data.images[pathlib.Path(template_name).name]
|
||||
img.name = name
|
||||
return img
|
||||
|
||||
|
|
@ -47,7 +48,7 @@ class ImBufTest(AbstractImBufTest):
|
|||
name += str(settings[s]).rjust(2, '0') + "-"
|
||||
# do not embed exr quality into test file name unless it is DWAA/DWAB
|
||||
elif not (s == "quality" and ext == "exr" and
|
||||
settings.get("exr_codec") not in ("DWAA", "DWAB")):
|
||||
settings.get("exr_codec") not in ("DWAA", "DWAB")):
|
||||
name += str(settings[s]) + "-"
|
||||
|
||||
setattr(image_settings, s, settings[s])
|
||||
|
|
@ -60,6 +61,7 @@ class ImBufTest(AbstractImBufTest):
|
|||
loaders = {
|
||||
"rgba08": lambda name: self._load_template_image(name, TEMPLATE_RGBA08),
|
||||
"rgba32": lambda name: self._load_template_image(name, TEMPLATE_RGBA32),
|
||||
"high-values": lambda name: self._load_template_image(name, TEMPLATE_HIGH_VALUES),
|
||||
}
|
||||
|
||||
# Load the template image and assign it the image name
|
||||
|
|
@ -144,6 +146,15 @@ class ImBufSaveTest(ImBufTest):
|
|||
self.check(src="rgba32", ext="exr", settings={"file_format": "OPEN_EXR", "color_mode": "RGB", "color_depth": "16", "exr_codec": "HTJ2K"})
|
||||
self.check(src="rgba32", ext="exr", settings={"file_format": "OPEN_EXR", "color_mode": "RGBA", "color_depth": "32", "exr_codec": "HTJ2K"})
|
||||
|
||||
def test_save_exr_dwa_high_values(self):
|
||||
self.skip_if_format_missing("OPENEXR")
|
||||
|
||||
# Tests for correct clamping of values that exceed HALF_MAX.
|
||||
self.check(src="high-values", ext="exr", settings={"file_format": "OPEN_EXR", "color_mode": "RGB", "color_depth": "16", "exr_codec": "DWAA", "quality": 100})
|
||||
self.check(src="high-values", ext="exr", settings={"file_format": "OPEN_EXR", "color_mode": "RGB", "color_depth": "16", "exr_codec": "DWAA", "quality": 90})
|
||||
self.check(src="high-values", ext="exr", settings={"file_format": "OPEN_EXR", "color_mode": "RGB", "color_depth": "16", "exr_codec": "DWAA", "quality": 75})
|
||||
self.check(src="high-values", ext="exr", settings={"file_format": "OPEN_EXR", "color_mode": "RGB", "color_depth": "16", "exr_codec": "DWAA", "quality": 50})
|
||||
|
||||
def test_save_hdr(self):
|
||||
self.check(src="rgba08", ext="hdr", settings={"file_format": "HDR", "color_mode": "BW"})
|
||||
self.check(src="rgba08", ext="hdr", settings={"file_format": "HDR", "color_mode": "RGB"})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue