Skip to content

Commit

Permalink
Merge pull request #256 from Tom94/color-profiles
Browse files Browse the repository at this point in the history
Color profiles
  • Loading branch information
Tom94 authored Feb 23, 2025
2 parents b037470 + dee1874 commit b05d23d
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 5 deletions.
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,6 @@
path = dependencies/aom
url = https://github.com/Tom94/aom
shallow = true
[submodule "dependencies/Little-CMS"]
path = dependencies/Little-CMS
url = https://github.com/Tom94/Little-CMS
4 changes: 2 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ if (MSVC)
set(TEV_LIBS ${TEV_LIBS} zlibstatic DirectXTex wsock32 ws2_32)
endif()
if (TEV_USE_LIBHEIF)
set(TEV_LIBS ${TEV_LIBS} heif)
set(TEV_LIBS ${TEV_LIBS} lcms2 heif)
endif()

set(TEV_SOURCES
Expand Down Expand Up @@ -236,7 +236,7 @@ include_directories(
"${CMAKE_CURRENT_SOURCE_DIR}/include"
)
if (TEV_USE_LIBHEIF)
include_directories(${LIBHEIF_INCLUDE})
include_directories(${LCMS_INCLUDE} ${LIBHEIF_INCLUDE})
endif()

set(TEV_DEFINITIONS -DTEV_VERSION="${TEV_VERSION_ARCH}")
Expand Down
11 changes: 10 additions & 1 deletion dependencies/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,9 @@ if (TEV_SUPPORT_HEIC)
set(LIBDE265_LIBRARY de265)
endif()

# Compile libheif
if (TEV_USE_LIBHEIF)
# Compile libheif

# General build config
set(BUILD_SHARED_LIBS OFF CACHE BOOL " " FORCE)
set(BUILD_TESTING OFF CACHE BOOL " " FORCE)
Expand Down Expand Up @@ -101,6 +102,14 @@ if (TEV_USE_LIBHEIF)
target_include_directories(heif PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/libde265")

set(LIBHEIF_INCLUDE "${CMAKE_CURRENT_SOURCE_DIR}/libheif/libheif/api" "${CMAKE_CURRENT_BINARY_DIR}/libheif" PARENT_SCOPE)

# Compile Little-CMS for ICC color profile handling
set(BUILD_SHARED_LIBS OFF CACHE BOOL " " FORCE)
set(BUILD_TOOLS OFF CACHE BOOL " " FORCE)
set(BUILD_TESTS OFF CACHE BOOL " " FORCE)
add_subdirectory(Little-CMS EXCLUDE_FROM_ALL)

set(LCMS_INCLUDE "${CMAKE_CURRENT_SOURCE_DIR}/Little-CMS/include" PARENT_SCOPE)
endif()

# Compile OpenEXR
Expand Down
1 change: 1 addition & 0 deletions dependencies/Little-CMS
Submodule Little-CMS added at bc08d5
1 change: 1 addition & 0 deletions src/HelpWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ HelpWindow::HelpWindow(Widget* parent, bool supportsHdr, function<void()> closeC
#endif
#ifdef TEV_USE_LIBHEIF
addLibrary(about, "libheif", "", "HEIF and avif file format decoder and encoder");
addLibrary(about, "Little-CMS", "", "FOSS CMM engine. Fast transforms between ICC profiles.");
#endif
addLibrary(about, "NanoGUI", "", "Small GUI library");
addLibrary(about, "NanoVG", "", "Small vector graphics library");
Expand Down
144 changes: 142 additions & 2 deletions src/imageio/HeifImageLoader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

#include <libheif/heif.h>

#include <lcms2.h>

#include <ImfChromaticities.h>

using namespace nanogui;
using namespace std;

Expand All @@ -26,7 +30,7 @@ bool HeifImageLoader::canLoadFile(istream& iStream) const {
return heif_check_filetype(header, 12) == heif_filetype_yes_supported;
}

Task<vector<ImageData>> HeifImageLoader::load(istream& iStream, const fs::path&, const string& channelSelector, int priority) const {
Task<vector<ImageData>> HeifImageLoader::load(istream& iStream, const fs::path&, const string&, int priority) const {
vector<ImageData> result(1);
ImageData& resultData = result.front();

Expand Down Expand Up @@ -115,8 +119,113 @@ Task<vector<ImageData>> HeifImageLoader::load(istream& iStream, const fs::path&,
throw invalid_argument{"Faild to get image data."};
}

auto getCmsTransform = [&]() {
size_t profileSize = heif_image_handle_get_raw_color_profile_size(handle);
if (profileSize == 0) {
return (cmsHTRANSFORM) nullptr;
}

vector<uint8_t> profileData(profileSize);
if (auto error = heif_image_handle_get_raw_color_profile(handle, profileData.data()); error.code != heif_error_Ok) {
if (error.code == heif_error_Color_profile_does_not_exist) {
return (cmsHTRANSFORM) nullptr;
}

tlog::warning() << "Failed to read ICC profile: " << error.message;
return (cmsHTRANSFORM) nullptr;
}

cmsSetLogErrorHandler([](cmsContext, cmsUInt32Number errorCode, const char* message) {
tlog::error() << fmt::format("lcms error #{}: {}", errorCode, message);
});

// Create ICC profile from the raw data
cmsHPROFILE srcProfile = cmsOpenProfileFromMem(profileData.data(), (cmsUInt32Number)profileSize);
if (!srcProfile) {
tlog::warning() << "Failed to create ICC profile from raw data";
return (cmsHTRANSFORM) nullptr;
}

ScopeGuard srcProfileGuard{[srcProfile] { cmsCloseProfile(srcProfile); }};

cmsCIExyY D65 = {0.3127, 0.3290, 1.0};
cmsCIExyYTRIPLE Rec709Primaries = {
{0.6400, 0.3300, 1.0},
{0.3000, 0.6000, 1.0},
{0.1500, 0.0600, 1.0}
};

cmsToneCurve* linearCurve[3];
linearCurve[0] = linearCurve[1] = linearCurve[2] = cmsBuildGamma(0, 1.0f);

cmsHPROFILE rec709Profile = cmsCreateRGBProfile(&D65, &Rec709Primaries, linearCurve);

if (!rec709Profile) {
tlog::warning() << "Failed to create Rec.709 color profile";
return (cmsHTRANSFORM) nullptr;
}

ScopeGuard rec709ProfileGuard{[rec709Profile] { cmsCloseProfile(rec709Profile); }};

// Create transform from source profile to Rec.709
auto type = numChannels == 4 ? (hasPremultipliedAlpha ? TYPE_RGBA_FLT_PREMUL : TYPE_RGBA_FLT) : TYPE_RGB_FLT;
cmsHTRANSFORM transform = cmsCreateTransform(srcProfile, type, rec709Profile, type, INTENT_PERCEPTUAL, cmsFLAGS_NOCACHE);
if (!transform) {
tlog::warning() << "Failed to create color transform from ICC profile to Rec.709";
return (cmsHTRANSFORM) nullptr;
}

return transform;
};

resultData.channels = makeNChannels(numChannels, size);
resultData.hasPremultipliedAlpha = hasPremultipliedAlpha;

// If we've got an ICC color profile, apply that because it's the most detailed / standardized.
auto transform = getCmsTransform();
if (transform) {
ScopeGuard transformGuard{[transform] { cmsDeleteTransform(transform); }};

tlog::debug() << "Found ICC color profile.";

size_t numPixels = (size_t)size.x() * size.y();
vector<float> src(numPixels * numChannels);
vector<float> dst(numPixels * numChannels);

const size_t n_samples_per_row = size.x() * numChannels;
co_await ThreadPool::global().parallelForAsync<size_t>(
0,
size.y(),
[&](int y) {
size_t offset = y * (size_t)n_samples_per_row;
for (size_t x = 0; x < n_samples_per_row; ++x) {
const uint16_t* typedData = reinterpret_cast<const uint16_t*>(data + y * bytesPerLine);
src[offset + x] = (float)typedData[x] * channelScale;
}

// Armchair parallelization of lcms: cmsDoTransform is reentrant per the spec, i.e. it can be called from multiple threads.
// So: call cmsDoTransform for each row in parallel.
cmsDoTransform(transform, &src[offset], &dst[offset], size.x());
},
priority
);

co_await ThreadPool::global().parallelForAsync<size_t>(
0,
numPixels,
[&](size_t i) {
for (size_t c = 0; c < numChannels; ++c) {
resultData.channels[c].at(i) = dst[i * numChannels + c];
}
},
priority
);

co_return result;
}

// Otherwise, assume the image is in Rec.709/sRGB and convert it to linear space, followed by an optional change in color space if an
// NCLX profile is present.
co_await ThreadPool::global().parallelForAsync<int>(
0,
size.y(),
Expand All @@ -125,6 +234,7 @@ Task<vector<ImageData>> HeifImageLoader::load(istream& iStream, const fs::path&,
size_t i = y * (size_t)size.x() + x;
auto typedData = reinterpret_cast<const unsigned short*>(data + y * bytesPerLine);
int baseIdx = x * numChannels;

for (int c = 0; c < numChannels; ++c) {
if (c == 3) {
resultData.channels[c].at(i) = typedData[baseIdx + c] * channelScale;
Expand All @@ -137,7 +247,37 @@ Task<vector<ImageData>> HeifImageLoader::load(istream& iStream, const fs::path&,
priority
);

resultData.hasPremultipliedAlpha = hasPremultipliedAlpha;
heif_color_profile_nclx* nclx = nullptr;
if (auto error = heif_image_handle_get_nclx_color_profile(handle, &nclx); error.code != heif_error_Ok) {
if (error.code == heif_error_Color_profile_does_not_exist) {
co_return result;
}

tlog::warning() << "Failed to read ICC profile: " << error.message;
co_return result;
}

ScopeGuard nclxGuard{[nclx] { heif_nclx_color_profile_free(nclx); }};

tlog::debug() << "Found NCLX color profile.";

// Only convert if not already in Rec.709/sRGB
if (nclx->color_primaries != heif_color_primaries_ITU_R_BT_709_5) {
Imf::Chromaticities rec709; // default rec709 (sRGB) primaries
Imf::Chromaticities chroma = {
{nclx->color_primary_red_x, nclx->color_primary_red_y },
{nclx->color_primary_green_x, nclx->color_primary_green_y},
{nclx->color_primary_blue_x, nclx->color_primary_blue_y },
{nclx->color_primary_white_x, nclx->color_primary_white_y}
};

Imath::M44f M = Imf::RGBtoXYZ(chroma, 1) * Imf::XYZtoRGB(rec709, 1);
for (int m = 0; m < 4; ++m) {
for (int n = 0; n < 4; ++n) {
resultData.toRec709.m[m][n] = M.x[m][n];
}
}
}

co_return result;
}
Expand Down

0 comments on commit b05d23d

Please sign in to comment.