Color Management: Save and load HDR images with 203 nits
Based on extensive testing, this gives matching HDR brighness across most application, with both PNG and AVIF. Video remains at 100 nits as that appears to tbe convention there. This is implemented by adding two modified PQ and HLG color spaces to the OCIO config on startup, and for the specific cases of image save and loaded these will replace the regular PQ and HLG color spaces. This was chosen rather than adding them as color spaces in the OCIO config, so that it can work for any config with appropriate interop IDs, including the ACES config. Additionally, it would be unclear how to make this work with view + display transforms, we wouldn't want to burden the users with having to pick a different display depending if they are saving images or video. Ref #145855, #144911 Pull Request: https://projects.blender.org/blender/blender/pulls/146888
This commit is contained in:
@@ -37,8 +37,10 @@ enum ColorManagedDisplaySpace {
|
||||
/* Convert to display space for drawing. This will included emulation of the
|
||||
* chosen display for an extended sRGB buffer. */
|
||||
DISPLAY_SPACE_DRAW,
|
||||
/* Convert to display space for file output. */
|
||||
DISPLAY_SPACE_FILE_OUTPUT,
|
||||
/* Convert to display space for file output. Note image and video have different
|
||||
* conventions for HDR brightness, so there is a distinction. */
|
||||
DISPLAY_SPACE_IMAGE_OUTPUT,
|
||||
DISPLAY_SPACE_VIDEO_OUTPUT,
|
||||
/* Convert to display space for inspecting color values as text in the UI. */
|
||||
DISPLAY_SPACE_COLOR_INSPECTION,
|
||||
};
|
||||
|
||||
@@ -34,4 +34,4 @@ const ColorSpace *colormanage_colorspace_get_named(const char *name);
|
||||
const ColorSpace *colormanage_colorspace_get_roled(int role);
|
||||
|
||||
void colormanage_imbuf_set_default_spaces(ImBuf *ibuf);
|
||||
void colormanage_imbuf_make_linear(ImBuf *ibuf, const char *from_colorspace);
|
||||
void colormanage_imbuf_make_linear(ImBuf *ibuf, const char *from_colorspace, bool video);
|
||||
|
||||
@@ -819,6 +819,7 @@ static std::shared_ptr<const ocio::CPUProcessor> get_display_buffer_processor(
|
||||
display_parameters.use_hdr_buffer = GPU_hdr_support();
|
||||
display_parameters.use_hdr_display = IMB_colormanagement_display_is_hdr(&display_settings,
|
||||
view_transform);
|
||||
display_parameters.is_image_output = (target == DISPLAY_SPACE_IMAGE_OUTPUT);
|
||||
display_parameters.use_display_emulation = (target == DISPLAY_SPACE_DRAW) ?
|
||||
get_display_emulation(display_settings) :
|
||||
false;
|
||||
@@ -870,7 +871,7 @@ void colormanage_imbuf_set_default_spaces(ImBuf *ibuf)
|
||||
ibuf->byte_buffer.colorspace = g_config->get_color_space(global_role_default_byte);
|
||||
}
|
||||
|
||||
void colormanage_imbuf_make_linear(ImBuf *ibuf, const char *from_colorspace)
|
||||
void colormanage_imbuf_make_linear(ImBuf *ibuf, const char *from_colorspace, bool video)
|
||||
{
|
||||
const ColorSpace *colorspace = g_config->get_color_space(from_colorspace);
|
||||
|
||||
@@ -887,6 +888,14 @@ void colormanage_imbuf_make_linear(ImBuf *ibuf, const char *from_colorspace)
|
||||
IMB_free_byte_pixels(ibuf);
|
||||
}
|
||||
|
||||
if (!video) {
|
||||
const ColorSpace *image_colorspace = g_config->get_color_space_for_hdr_image(
|
||||
from_colorspace);
|
||||
if (image_colorspace) {
|
||||
from_colorspace = image_colorspace->name().c_str();
|
||||
}
|
||||
}
|
||||
|
||||
IMB_colormanagement_transform_float(ibuf->float_buffer.data,
|
||||
ibuf->x,
|
||||
ibuf->y,
|
||||
@@ -2720,7 +2729,9 @@ ImBuf *IMB_colormanagement_imbuf_for_write(ImBuf *ibuf,
|
||||
colormanagement_imbuf_make_display_space(colormanaged_ibuf,
|
||||
&image_format->view_settings,
|
||||
&image_format->display_settings,
|
||||
DISPLAY_SPACE_FILE_OUTPUT,
|
||||
image_format->media_type == MEDIA_TYPE_VIDEO ?
|
||||
DISPLAY_SPACE_VIDEO_OUTPUT :
|
||||
DISPLAY_SPACE_IMAGE_OUTPUT,
|
||||
byte_output);
|
||||
|
||||
if (colormanaged_ibuf->float_buffer.data) {
|
||||
@@ -2751,6 +2762,14 @@ ImBuf *IMB_colormanagement_imbuf_for_write(ImBuf *ibuf,
|
||||
|
||||
const char *to_colorspace = image_format->linear_colorspace_settings.name;
|
||||
|
||||
/* to_colorspace may need to modified to compensate for 100 vs 203 nits conventions. */
|
||||
if (image_format->media_type != MEDIA_TYPE_VIDEO) {
|
||||
const ColorSpace *image_colorspace = g_config->get_color_space_for_hdr_image(to_colorspace);
|
||||
if (image_colorspace) {
|
||||
to_colorspace = image_colorspace->name().c_str();
|
||||
}
|
||||
}
|
||||
|
||||
/* TODO: can we check with OCIO if color spaces are the same but have different names? */
|
||||
if (to_colorspace[0] == '\0' || STREQ(from_colorspace, to_colorspace)) {
|
||||
/* No conversion needed, but may still need to allocate byte buffer for output. */
|
||||
|
||||
@@ -117,7 +117,7 @@ static void imb_handle_colorspace_and_alpha(ImBuf *ibuf,
|
||||
}
|
||||
}
|
||||
|
||||
colormanage_imbuf_make_linear(ibuf, new_colorspace);
|
||||
colormanage_imbuf_make_linear(ibuf, new_colorspace, false);
|
||||
}
|
||||
|
||||
ImBuf *IMB_load_image_from_memory(const uchar *mem,
|
||||
|
||||
@@ -1329,7 +1329,7 @@ static ImBuf *ffmpeg_fetchibuf(MovieReader *anim, int position, IMB_Timecode_Typ
|
||||
* It might not be the most optimal thing to do from the playback performance in the
|
||||
* sequencer perspective, but it ensures that other areas in Blender do not run into obscure
|
||||
* color space mismatches. */
|
||||
colormanage_imbuf_make_linear(cur_frame_final, anim->colorspace);
|
||||
colormanage_imbuf_make_linear(cur_frame_final, anim->colorspace, true);
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -34,6 +34,8 @@ struct DisplayParameters {
|
||||
bool use_hdr_buffer = false;
|
||||
/* Chosen display is HDR. */
|
||||
bool use_hdr_display = false;
|
||||
/* Display transform is being used for image output. */
|
||||
bool is_image_output = false;
|
||||
/* Rather than outputting colors for the specified display, output extended
|
||||
* sRGB colors emulating the specified display. */
|
||||
bool use_display_emulation = false;
|
||||
@@ -133,6 +135,12 @@ class Config {
|
||||
*/
|
||||
virtual const ColorSpace *get_color_space_by_interop_id(StringRefNull interop_id) const = 0;
|
||||
|
||||
/**
|
||||
* Get colorspace to be used for saving and loading HDR image files, which
|
||||
* may need adjustments compared to the colorspace as chosen by the user.
|
||||
**/
|
||||
virtual const ColorSpace *get_color_space_for_hdr_image(StringRefNull name) const = 0;
|
||||
|
||||
/** \} */
|
||||
|
||||
/* -------------------------------------------------------------------- */
|
||||
|
||||
@@ -113,6 +113,11 @@ const ColorSpace *FallbackConfig::get_color_space_by_interop_id(StringRefNull in
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const ColorSpace *FallbackConfig::get_color_space_for_hdr_image(StringRefNull name) const
|
||||
{
|
||||
return get_color_space(name);
|
||||
}
|
||||
|
||||
/** \} */
|
||||
|
||||
/* -------------------------------------------------------------------- */
|
||||
|
||||
@@ -50,6 +50,7 @@ class FallbackConfig : public Config {
|
||||
const ColorSpace *get_color_space_by_index(int index) const override;
|
||||
const ColorSpace *get_sorted_color_space_by_index(int index) const override;
|
||||
const ColorSpace *get_color_space_by_interop_id(StringRefNull interop_id) const override;
|
||||
const ColorSpace *get_color_space_for_hdr_image(StringRefNull name) const override;
|
||||
|
||||
/* Working space API. */
|
||||
void set_scene_linear_role(StringRefNull name) override;
|
||||
|
||||
@@ -80,6 +80,7 @@ LibOCIOConfig::LibOCIOConfig(const OCIO_NAMESPACE::ConstConfigRcPtr &ocio_config
|
||||
|
||||
initialize_active_color_spaces();
|
||||
initialize_inactive_color_spaces();
|
||||
initialize_hdr_color_spaces();
|
||||
initialize_looks();
|
||||
initialize_displays();
|
||||
}
|
||||
@@ -366,6 +367,62 @@ const ColorSpace *LibOCIOConfig::get_color_space_by_interop_id(StringRefNull int
|
||||
|
||||
/** \} */
|
||||
|
||||
/* -------------------------------------------------------------------- */
|
||||
/** \name HDR image API
|
||||
* \{ */
|
||||
|
||||
const ColorSpace *LibOCIOConfig::get_color_space_for_hdr_image(StringRefNull name) const
|
||||
{
|
||||
/* Based on emperical testing, ideo works with 100 nits diffuse white, while
|
||||
* images need 203 nits diffuse whites to show matching results. */
|
||||
const ColorSpace *colorspece = get_color_space(name);
|
||||
if (colorspece->interop_id() == "pq_rec2020_display") {
|
||||
return get_color_space("blender:pq_rec2020_display_203nits");
|
||||
}
|
||||
if (colorspece->interop_id() == "hlg_rec2020_display") {
|
||||
return get_color_space("blender:hlg_rec2020_display_203nits");
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void LibOCIOConfig::initialize_hdr_color_spaces()
|
||||
{
|
||||
for (StringRefNull interop_id : {"pq_rec2020_display", "hlg_rec2020_display"}) {
|
||||
const auto *colorspace = static_cast<const LibOCIOColorSpace *>(
|
||||
get_color_space_by_interop_id(interop_id));
|
||||
if (!colorspace || !colorspace->is_display_referred()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Create colorspace that uses 203 nits diffuse white instead of 100 nits. */
|
||||
const auto hdr_100_colorspace = ocio_config_->getColorSpace(colorspace->name().c_str());
|
||||
const auto hdr_colorspace = OCIO_NAMESPACE::ColorSpace::Create(
|
||||
OCIO_NAMESPACE::REFERENCE_SPACE_DISPLAY);
|
||||
const auto group = OCIO_NAMESPACE::GroupTransform::Create();
|
||||
|
||||
hdr_colorspace->setName(("blender:" + interop_id + "_203nits").c_str());
|
||||
|
||||
const auto to_203_nits = OCIO_NAMESPACE::MatrixTransform::Create();
|
||||
to_203_nits->setMatrix(double4x4(double3x3::diagonal(203.0 / 100.0)).base_ptr());
|
||||
group->appendTransform(to_203_nits);
|
||||
|
||||
const auto to_display = hdr_100_colorspace
|
||||
->getTransform(OCIO_NAMESPACE::COLORSPACE_DIR_FROM_REFERENCE)
|
||||
->createEditableCopy();
|
||||
group->appendTransform(to_display);
|
||||
|
||||
hdr_colorspace->setTransform(group, OCIO_NAMESPACE::COLORSPACE_DIR_FROM_REFERENCE);
|
||||
|
||||
OCIO_NAMESPACE::Config *mutable_ocio_config = const_cast<OCIO_NAMESPACE::Config *>(
|
||||
ocio_config_.get());
|
||||
mutable_ocio_config->addColorSpace(hdr_colorspace);
|
||||
|
||||
inactive_color_spaces_.append_as(inactive_color_spaces_.size(), ocio_config_, hdr_colorspace);
|
||||
}
|
||||
}
|
||||
|
||||
/** \} */
|
||||
|
||||
/* -------------------------------------------------------------------- */
|
||||
/** \name Working space API
|
||||
* \{ */
|
||||
|
||||
@@ -60,6 +60,7 @@ class LibOCIOConfig : public Config {
|
||||
const ColorSpace *get_color_space_by_index(int index) const override;
|
||||
const ColorSpace *get_sorted_color_space_by_index(int index) const override;
|
||||
const ColorSpace *get_color_space_by_interop_id(StringRefNull interop_id) const override;
|
||||
const ColorSpace *get_color_space_for_hdr_image(StringRefNull name) const override;
|
||||
|
||||
/* Working space API. */
|
||||
void set_scene_linear_role(StringRefNull name) override;
|
||||
@@ -103,6 +104,7 @@ class LibOCIOConfig : public Config {
|
||||
* OpenColorIO configuration. */
|
||||
void initialize_active_color_spaces();
|
||||
void initialize_inactive_color_spaces();
|
||||
void initialize_hdr_color_spaces();
|
||||
void initialize_looks();
|
||||
void initialize_displays();
|
||||
};
|
||||
|
||||
@@ -96,6 +96,36 @@ static OCIO_NAMESPACE::TransformRcPtr create_extended_srgb_transform(
|
||||
return to_ui;
|
||||
}
|
||||
|
||||
static void adjust_for_hdr_image_file(const LibOCIOConfig &config,
|
||||
OCIO_NAMESPACE::GroupTransformRcPtr &group,
|
||||
StringRefNull display_name,
|
||||
StringRefNull view_name)
|
||||
{
|
||||
/* Convert HDR PQ and HLG images from 100 nits to 203 nits convention. */
|
||||
const LibOCIODisplay *display = static_cast<const LibOCIODisplay *>(
|
||||
config.get_display_by_name(display_name));
|
||||
const LibOCIOView *view = (display) ? static_cast<const LibOCIOView *>(
|
||||
display->get_view_by_name(view_name)) :
|
||||
nullptr;
|
||||
const LibOCIOColorSpace *display_colorspace = static_cast<const LibOCIOColorSpace *>(
|
||||
view->display_colorspace());
|
||||
|
||||
if (display_colorspace == nullptr || !display_colorspace->is_display_referred()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ColorSpace *image_display_colorspace = config.get_color_space_for_hdr_image(
|
||||
display_colorspace->name());
|
||||
if (image_display_colorspace == nullptr || image_display_colorspace == display_colorspace) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto to_display_linear = OCIO_NAMESPACE::ColorSpaceTransform::Create();
|
||||
to_display_linear->setSrc(display_colorspace->name().c_str());
|
||||
to_display_linear->setDst(image_display_colorspace->name().c_str());
|
||||
group->appendTransform(to_display_linear);
|
||||
}
|
||||
|
||||
static void display_as_extended_srgb(const LibOCIOConfig &config,
|
||||
OCIO_NAMESPACE::GroupTransformRcPtr &group,
|
||||
StringRefNull display_name,
|
||||
@@ -417,6 +447,11 @@ OCIO_NAMESPACE::ConstProcessorRcPtr create_ocio_display_processor(
|
||||
group->appendTransform(et);
|
||||
}
|
||||
|
||||
if (display_parameters.is_image_output) {
|
||||
adjust_for_hdr_image_file(
|
||||
config, group, display_parameters.display, display_parameters.view);
|
||||
}
|
||||
|
||||
/* Convert to extended sRGB to match the system graphics buffer. */
|
||||
if (display_parameters.use_display_emulation) {
|
||||
display_as_extended_srgb(config,
|
||||
|
||||
@@ -156,7 +156,7 @@ class ConvertToDisplayOperation : public NodeOperation {
|
||||
{
|
||||
const NodeConvertToDisplay &nctd = node_storage(bnode());
|
||||
ColormanageProcessor *color_processor = IMB_colormanagement_display_processor_new(
|
||||
&nctd.view_settings, &nctd.display_settings, DISPLAY_SPACE_FILE_OUTPUT, do_inverse());
|
||||
&nctd.view_settings, &nctd.display_settings, DISPLAY_SPACE_VIDEO_OUTPUT, do_inverse());
|
||||
|
||||
Result &input_image = get_input("Image");
|
||||
|
||||
@@ -181,7 +181,7 @@ class ConvertToDisplayOperation : public NodeOperation {
|
||||
{
|
||||
const NodeConvertToDisplay &nctd = node_storage(bnode());
|
||||
ColormanageProcessor *color_processor = IMB_colormanagement_display_processor_new(
|
||||
&nctd.view_settings, &nctd.display_settings, DISPLAY_SPACE_FILE_OUTPUT, do_inverse());
|
||||
&nctd.view_settings, &nctd.display_settings, DISPLAY_SPACE_VIDEO_OUTPUT, do_inverse());
|
||||
|
||||
Result &input_image = get_input("Image");
|
||||
float4 color = input_image.get_single_value<float4>();
|
||||
|
||||
Reference in New Issue
Block a user