From b09bf381b4d6cc0f6c8cc058987e6e3eed0b256a Mon Sep 17 00:00:00 2001 From: Alberto Griggio Date: Tue, 20 Mar 2018 15:06:09 +0100 Subject: [PATCH 1/2] added possibility to specify extra working spaces via a json file The JSON file is called workingspaces.json, it can be either in the global iccprofiles directory, or in the user's ICC profiles dir (set in preferences). The format is the following: {"working_spaces": [ { "name" : "ACES", "file" : "/path/to/ACES.icc" }, { "name" : "ACEScg", "matrix" : [0.7184354, 0.16578523, 0.09882643, 0.29728935, 0.66958117, 0.03571544, -0.00647622, 0.01469771, 0.66732561] } ]} if "matrix" is present, "file" is ignored. If only "file" is present, the matrix is extracted from the ICC profile. For this, we look only at the R, G, and B matrix columns and the white point set in the profile. Bradford adaptation is used to convert the matrix to D50. Anything else (LUT, TRC, ...) in the profile is ignored. It is the user's responsibility to ensure that the profile is suitable to be used as a working space. --- rtengine/clutstore.cc | 2 +- rtengine/dcp.cc | 48 +++--- rtengine/iccstore.cc | 313 +++++++++++++++++++++++++++++++++---- rtengine/iccstore.h | 4 +- rtengine/rawimagesource.cc | 29 +++- rtengine/rt_math.h | 63 ++++++++ rtgui/icmpanel.cc | 2 +- 7 files changed, 408 insertions(+), 53 deletions(-) diff --git a/rtengine/clutstore.cc b/rtengine/clutstore.cc index 565db8faa..4124e38bb 100644 --- a/rtengine/clutstore.cc +++ b/rtengine/clutstore.cc @@ -286,7 +286,7 @@ void rtengine::HaldCLUT::splitClutFilename( profile_name = "sRGB"; if (!name.empty()) { - for (const auto& working_profile : rtengine::ICCStore::getWorkingProfiles()) { + for (const auto& working_profile : rtengine::ICCStore::getInstance()->getWorkingProfiles()) { if ( !working_profile.empty() // This isn't strictly needed, but an empty wp name should be skipped anyway && std::search(name.rbegin(), name.rend(), working_profile.rbegin(), working_profile.rend()) == name.rbegin() diff --git a/rtengine/dcp.cc b/rtengine/dcp.cc index 82ea35f0f..d00827d73 100644 --- a/rtengine/dcp.cc +++ b/rtengine/dcp.cc @@ -37,30 +37,36 @@ namespace DCPProfile::Matrix invert3x3(const DCPProfile::Matrix& a) { - const double res00 = a[1][1] * a[2][2] - a[2][1] * a[1][2]; - const double res10 = a[2][0] * a[1][2] - a[1][0] * a[2][2]; - const double res20 = a[1][0] * a[2][1] - a[2][0] * a[1][1]; - - const double det = a[0][0] * res00 + a[0][1] * res10 + a[0][2] * res20; - - if (std::fabs(det) < 1.0e-10) { + DCPProfile::Matrix res = a; + if (!invertMatrix(a, res)) { std::cerr << "DCP matrix cannot be inverted! Expect weird output." << std::endl; - return a; } - - DCPProfile::Matrix res; - - res[0][0] = res00 / det; - res[0][1] = (a[2][1] * a[0][2] - a[0][1] * a[2][2]) / det; - res[0][2] = (a[0][1] * a[1][2] - a[1][1] * a[0][2]) / det; - res[1][0] = res10 / det; - res[1][1] = (a[0][0] * a[2][2] - a[2][0] * a[0][2]) / det; - res[1][2] = (a[1][0] * a[0][2] - a[0][0] * a[1][2]) / det; - res[2][0] = res20 / det; - res[2][1] = (a[2][0] * a[0][1] - a[0][0] * a[2][1]) / det; - res[2][2] = (a[0][0] * a[1][1] - a[1][0] * a[0][1]) / det; - return res; + + // const double res00 = a[1][1] * a[2][2] - a[2][1] * a[1][2]; + // const double res10 = a[2][0] * a[1][2] - a[1][0] * a[2][2]; + // const double res20 = a[1][0] * a[2][1] - a[2][0] * a[1][1]; + + // const double det = a[0][0] * res00 + a[0][1] * res10 + a[0][2] * res20; + + // if (std::fabs(det) < 1.0e-10) { + // std::cerr << "DCP matrix cannot be inverted! Expect weird output." << std::endl; + // return a; + // } + + // DCPProfile::Matrix res; + + // res[0][0] = res00 / det; + // res[0][1] = (a[2][1] * a[0][2] - a[0][1] * a[2][2]) / det; + // res[0][2] = (a[0][1] * a[1][2] - a[1][1] * a[0][2]) / det; + // res[1][0] = res10 / det; + // res[1][1] = (a[0][0] * a[2][2] - a[2][0] * a[0][2]) / det; + // res[1][2] = (a[1][0] * a[0][2] - a[0][0] * a[1][2]) / det; + // res[2][0] = res20 / det; + // res[2][1] = (a[2][0] * a[0][1] - a[0][0] * a[2][1]) / det; + // res[2][2] = (a[0][0] * a[1][1] - a[1][0] * a[0][1]) / det; + + // return res; } DCPProfile::Matrix multiply3x3(const DCPProfile::Matrix& a, const DCPProfile::Matrix& b) diff --git a/rtengine/iccstore.cc b/rtengine/iccstore.cc index e77ea7f0f..50ff58878 100644 --- a/rtengine/iccstore.cc +++ b/rtengine/iccstore.cc @@ -27,6 +27,8 @@ #include #endif +#include + #include "iccstore.h" #include "iccmatrices.h" @@ -35,6 +37,8 @@ #include "../rtgui/options.h" #include "../rtgui/threadutils.h" +#include "cJSON.h" + namespace rtengine { @@ -274,7 +278,7 @@ public: for (int i = 0; i < N; ++i) { wProfiles[wpnames[i]] = createFromMatrix(wprofiles[i]); - wProfilesGamma[wpnames[i]] = createFromMatrix(wprofiles[i], true); + // wProfilesGamma[wpnames[i]] = createFromMatrix(wprofiles[i], true); wMatrices[wpnames[i]] = wprofiles[i]; iwMatrices[wpnames[i]] = iwprofiles[i]; } @@ -287,11 +291,11 @@ public: cmsCloseProfile(p.second); } } - for (auto &p : wProfilesGamma) { - if (p.second) { - cmsCloseProfile(p.second); - } - } + // for (auto &p : wProfilesGamma) { + // if (p.second) { + // cmsCloseProfile(p.second); + // } + // } for (auto &p : fileProfiles) { if(p.second) { cmsCloseProfile(p.second); @@ -334,6 +338,9 @@ public: defaultMonitorProfile = settings->monitorProfile; + loadWorkingSpaces(rtICCDir); + loadWorkingSpaces(userICCDir); + // initialize the alarm colours for lcms gamut checking -- we use bright green cmsUInt16Number cms_alarm_codes[cmsMAXCHANNELS] = { 0, 65535, 65535 }; cmsSetAlarmCodes(cms_alarm_codes); @@ -349,16 +356,16 @@ public: : wProfiles.find("sRGB")->second; } - cmsHPROFILE workingSpaceGamma(const Glib::ustring& name) const - { + // cmsHPROFILE workingSpaceGamma(const Glib::ustring& name) const + // { - const ProfileMap::const_iterator r = wProfilesGamma.find(name); + // const ProfileMap::const_iterator r = wProfilesGamma.find(name); - return - r != wProfilesGamma.end() - ? r->second - : wProfilesGamma.find("sRGB")->second; - } + // return + // r != wProfilesGamma.end() + // ? r->second + // : wProfilesGamma.find("sRGB")->second; + // } TMatrix workingSpaceMatrix(const Glib::ustring& name) const { @@ -582,17 +589,276 @@ public: defaultMonitorProfile = name; } + std::vector getWorkingProfiles() + { + std::vector res; + + // for (unsigned int i = 0; i < sizeof(wpnames) / sizeof(wpnames[0]); i++) { + // res.push_back(wpnames[i]); + // } + for (const auto &p : wProfiles) { + res.push_back(p.first); + } + + return res; + } + private: + using CVector = std::array; + using CMatrix = std::array; + struct PMatrix { + double matrix[3][3]; + PMatrix(): matrix{} {} + PMatrix(const CMatrix &m) + { + set(m); + } + + CMatrix toMatrix() const + { + CMatrix ret; + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 3; ++j) { + ret[i][j] = matrix[i][j]; + } + } + return ret; + } + + void set(const CMatrix &m) + { + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 3; ++j) { + matrix[i][j] = m[i][j]; + } + } + } + }; + + bool computeWorkingSpaceMatrix(const Glib::ustring &path, const Glib::ustring &filename, PMatrix &out) + { + Glib::ustring fullpath = filename; + if (!Glib::path_is_absolute(fullpath)) { + fullpath = Glib::build_filename(path, filename); + } + ProfileContent content(fullpath); + cmsHPROFILE prof = content.toProfile(); + if (!prof) { + return false; + } + if (cmsGetColorSpace(prof) != cmsSigRgbData) { + cmsCloseProfile(prof); + return false; + } + if (!cmsIsMatrixShaper(prof)) { + cmsCloseProfile(prof); + return false; + } + cmsCIEXYZ *white = static_cast(cmsReadTag(prof, cmsSigMediaWhitePointTag)); + cmsCIEXYZ *red = static_cast(cmsReadTag(prof, cmsSigRedMatrixColumnTag)); + cmsCIEXYZ *green = static_cast(cmsReadTag(prof, cmsSigGreenMatrixColumnTag)); + cmsCIEXYZ *blue = static_cast(cmsReadTag(prof, cmsSigBlueMatrixColumnTag)); + + if (!white || !red || !green || !blue) { + cmsCloseProfile(prof); + return false; + } + + // do the Bradford adaptation to D50 + // matrices from Bruce Lindbloom's webpage + static constexpr CMatrix bradford_MA = { + CVector({0.8951000, 0.2664000, -0.1614000}), + CVector({-0.7502000, 1.7135000, 0.0367000}), + CVector({0.0389000, -0.0685000, 1.0296000}) + }; + static constexpr CMatrix bradford_MA_inv = { + CVector({0.9869929, -0.1470543, 0.1599627}), + CVector({0.4323053, 0.5183603, 0.0492912}), + CVector({-0.0085287, 0.0400428, 0.9684867}) + }; + static constexpr CVector bradford_MA_dot_D50 = { + 0.99628443, 1.02042736, 0.81864437 + }; + + CVector srcw = dotProduct(bradford_MA, CVector({ white->X, white->Y, white->Z })); + CMatrix m = { + CVector({ bradford_MA_dot_D50[0]/srcw[0], 0.0, 0.0 }), + CVector({ 0.0, bradford_MA_dot_D50[1]/srcw[1], 0.0 }), + CVector({ 0.0, 0.0, bradford_MA_dot_D50[2]/srcw[2] }) + }; + CMatrix adapt = dotProduct(dotProduct(bradford_MA_inv, m), bradford_MA); + + m[0][0] = red->X; m[0][1] = green->X; m[0][2] = blue->X; + m[1][0] = red->Y; m[1][1] = green->Y; m[1][2] = blue->Y; + m[2][0] = red->Z; m[2][1] = green->Z; m[2][2] = blue->Z; + + m = dotProduct(adapt, m); + out.set(m); + + cmsCloseProfile(prof); + return true; + } + + bool loadWorkingSpaces(const Glib::ustring &path) + { + Glib::ustring fileName = Glib::build_filename(path, "workingspaces.json"); + FILE* const f = g_fopen(fileName.c_str(), "r"); + + if (settings->verbose) { + std::cout << "trying to load extra working spaces from " << fileName << std::flush; + } + + if (!f) { + if (settings->verbose) { + std::cout << " FAIL" << std::endl; + } + return false; + } + + fseek(f, 0, SEEK_END); + long length = ftell(f); + if (length <= 0) { + if (settings->verbose) { + std::cout << " FAIL" << std::endl; + } + fclose(f); + return false; + } + + char *buf = new char[length + 1]; + fseek(f, 0, SEEK_SET); + length = fread(buf, 1, length, f); + buf[length] = 0; + + fclose(f); + + cJSON_Minify(buf); + cJSON *root = cJSON_Parse(buf); + + if (!root) { + if (settings->verbose) { + std::cout << " FAIL" << std::endl; + } + return false; + } + + delete[] buf; + + cJSON *js = cJSON_GetObjectItem(root, "working_spaces"); + if (!js) { + goto parse_error; + } + + for (js = js->child; js != nullptr; js = js->next) { + cJSON *ji = cJSON_GetObjectItem(js, "name"); + std::unique_ptr m(new PMatrix); + std::string name; + + if (!ji || ji->type != cJSON_String) { + goto parse_error; + } + + name = ji->valuestring; + + if (wProfiles.find(name) != wProfiles.end()) { + continue; // already there -- ignore + } + + bool found_matrix = false; + + ji = cJSON_GetObjectItem(js, "matrix"); + if (ji) { + if (ji->type != cJSON_Array) { + goto parse_error; + } + + ji = ji->child; + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 3; ++j, ji = ji->next) { + if (!ji || ji->type != cJSON_Number) { + goto parse_error; + } + m->matrix[i][j] = ji->valuedouble; + } + } + + if (ji) { + goto parse_error; + } + found_matrix = true; + } else { + ji = cJSON_GetObjectItem(js, "file"); + if (!ji || ji->type != cJSON_String) { + goto parse_error; + } + found_matrix = computeWorkingSpaceMatrix(path, ji->valuestring, *m); + } + + if (!found_matrix) { + if (settings->verbose) { + std::cout << "Could not find suitable matrix for working space: " << name << std::endl; + } + continue; + } + + pMatrices.emplace_back(std::move(m)); + TMatrix w = pMatrices.back()->matrix; + + CMatrix b = {}; + if (!rtengine::invertMatrix(pMatrices.back()->toMatrix(), b)) { + if (settings->verbose) { + std::cout << "Matrix for working space: " << name << " is not invertible, skipping" << std::endl; + } + pMatrices.pop_back(); + } else { + wMatrices[name] = w; + pMatrices.emplace_back(new PMatrix(b)); + TMatrix iw = pMatrices.back()->matrix; + iwMatrices[name] = iw; + wProfiles[name] = createFromMatrix(w); + + if (settings->verbose) { + std::cout << "Added working space: " << name << std::endl; + std::cout << " matrix: ["; + for (int i = 0; i < 3; ++i) { + std::cout << " ["; + for (int j = 0; j < 3; ++j) { + std::cout << " " << w[i][j]; + } + std::cout << "]"; + } + std::cout << " ]" << std::endl; + } + } + } + + cJSON_Delete(root); + if (settings->verbose) { + std::cout << " OK" << std::endl; + } + return true; + + parse_error: + if (settings->verbose) { + std::cout << " ERROR in parsing " << fileName << std::endl; + } + + cJSON_Delete(root); + return false; + } + using ProfileMap = std::map; using MatrixMap = std::map; using ContentMap = std::map; using NameMap = std::map; ProfileMap wProfiles; - ProfileMap wProfilesGamma; + // ProfileMap wProfilesGamma; MatrixMap wMatrices; MatrixMap iwMatrices; + std::vector> pMatrices; + // These contain profiles from user/system directory(supplied on init) Glib::ustring profilesDir; Glib::ustring userICCDir; @@ -630,10 +896,10 @@ cmsHPROFILE rtengine::ICCStore::workingSpace(const Glib::ustring& name) const return implementation->workingSpace(name); } -cmsHPROFILE rtengine::ICCStore::workingSpaceGamma(const Glib::ustring& name) const -{ - return implementation->workingSpaceGamma(name); -} +// cmsHPROFILE rtengine::ICCStore::workingSpaceGamma(const Glib::ustring& name) const +// { +// return implementation->workingSpaceGamma(name); +// } rtengine::TMatrix rtengine::ICCStore::workingSpaceMatrix(const Glib::ustring& name) const { @@ -736,14 +1002,7 @@ rtengine::ICCStore::~ICCStore() = default; std::vector rtengine::ICCStore::getWorkingProfiles() { - - std::vector res; - - for (unsigned int i = 0; i < sizeof(wpnames) / sizeof(wpnames[0]); i++) { - res.push_back(wpnames[i]); - } - - return res; + return implementation->getWorkingProfiles(); } std::vector rtengine::ICCStore::getGamma() diff --git a/rtengine/iccstore.h b/rtengine/iccstore.h index 4604404fb..c23ecd112 100644 --- a/rtengine/iccstore.h +++ b/rtengine/iccstore.h @@ -69,7 +69,7 @@ public: void init(const Glib::ustring& usrICCDir, const Glib::ustring& stdICCDir, bool loadAll); cmsHPROFILE workingSpace(const Glib::ustring& name) const; - cmsHPROFILE workingSpaceGamma(const Glib::ustring& name) const; + // cmsHPROFILE workingSpaceGamma(const Glib::ustring& name) const; TMatrix workingSpaceMatrix(const Glib::ustring& name) const; TMatrix workingSpaceInverseMatrix(const Glib::ustring& name) const; @@ -95,7 +95,7 @@ public: std::uint8_t getOutputIntents(const Glib::ustring& name) const; std::uint8_t getProofIntents(const Glib::ustring& name) const; - static std::vector getWorkingProfiles(); + /*static*/ std::vector getWorkingProfiles(); static std::vector getGamma(); static void getGammaArray(const procparams::ColorManagementParams& icm, GammaValues& ga); diff --git a/rtengine/rawimagesource.cc b/rtengine/rawimagesource.cc index 081ac3661..7fe347797 100644 --- a/rtengine/rawimagesource.cc +++ b/rtengine/rawimagesource.cc @@ -3997,7 +3997,7 @@ void RawImageSource::colorSpaceConversion_ (Imagefloat* im, const ColorManagemen } } else { - const bool working_space_is_prophoto = (cmp.working == "ProPhoto"); + bool working_space_is_prophoto = (cmp.working == "ProPhoto"); // use supplied input profile @@ -4066,6 +4066,33 @@ void RawImageSource::colorSpaceConversion_ (Imagefloat* im, const ColorManagemen cmsHPROFILE prophoto = ICCStore::getInstance()->workingSpace("ProPhoto"); // We always use Prophoto to apply the ICC profile to minimize problems with clipping in LUT conversion. bool transform_via_pcs_lab = false; bool separate_pcs_lab_highlights = false; + + // check if the working space is fully contained in prophoto + if (!working_space_is_prophoto) { + TMatrix toxyz = ICCStore::getInstance()->workingSpaceMatrix(cmp.working); + TMatrix torgb = ICCStore::getInstance()->workingSpaceInverseMatrix("ProPhoto"); + float rgb[3] = {0.f, 0.f, 0.f}; + for (int i = 0; i < 2 && !working_space_is_prophoto; ++i) { + rgb[i] = 1.f; + float x, y, z; + + Color::rgbxyz(rgb[0], rgb[1], rgb[2], x, y, z, toxyz); + Color::xyz2rgb(x, y, z, rgb[0], rgb[1], rgb[2], torgb); + + for (int j = 0; j < 2; ++j) { + if (rgb[j] < 0.f || rgb[j] > 1.f) { + working_space_is_prophoto = true; + prophoto = ICCStore::getInstance()->workingSpace(cmp.working); + if (settings->verbose) { + std::cout << "colorSpaceConversion_: converting directly to " << cmp.working << " instead of passing through ProPhoto" << std::endl; + } + break; + } + rgb[j] = 0.f; + } + } + } + lcmsMutex->lock (); switch (camera_icc_type) { diff --git a/rtengine/rt_math.h b/rtengine/rt_math.h index ca93619ee..dbf54ace5 100644 --- a/rtengine/rt_math.h +++ b/rtengine/rt_math.h @@ -4,6 +4,7 @@ #include #include #include +#include namespace rtengine { @@ -138,3 +139,65 @@ constexpr std::uint8_t uint16ToUint8Rounded(std::uint16_t i) } } + + +template +bool invertMatrix(const std::array, 3> &in, std::array, 3> &out) +{ + const T res00 = in[1][1] * in[2][2] - in[2][1] * in[1][2]; + const T res10 = in[2][0] * in[1][2] - in[1][0] * in[2][2]; + const T res20 = in[1][0] * in[2][1] - in[2][0] * in[1][1]; + + const T det = in[0][0] * res00 + in[0][1] * res10 + in[0][2] * res20; + + if (std::abs(det) < 1.0e-10) { + return false; + } + + out[0][0] = res00 / det; + out[0][1] = (in[2][1] * in[0][2] - in[0][1] * in[2][2]) / det; + out[0][2] = (in[0][1] * in[1][2] - in[1][1] * in[0][2]) / det; + out[1][0] = res10 / det; + out[1][1] = (in[0][0] * in[2][2] - in[2][0] * in[0][2]) / det; + out[1][2] = (in[1][0] * in[0][2] - in[0][0] * in[1][2]) / det; + out[2][0] = res20 / det; + out[2][1] = (in[2][0] * in[0][1] - in[0][0] * in[2][1]) / det; + out[2][2] = (in[0][0] * in[1][1] - in[1][0] * in[0][1]) / det; + + return true; +} + +template +std::array, 3> dotProduct(const std::array, 3> &a, const std::array, 3> &b) +{ + std::array, 3> res; + + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 3; ++j) { + res[i][j] = 0; + + for (int k = 0; k < 3; ++k) { + res[i][j] += a[i][k] * b[k][j]; + } + } + } + + return res; +} + + +template +std::array dotProduct(const std::array, 3> &a, const std::array &b) +{ + std::array res; + + for (int i = 0; i < 3; ++i) { + res[i] = 0; + for (int k = 0; k < 3; ++k) { + res[i] += a[i][k] * b[k]; + } + } + + return res; +} + diff --git a/rtgui/icmpanel.cc b/rtgui/icmpanel.cc index e026243be..bb9677d1c 100644 --- a/rtgui/icmpanel.cc +++ b/rtgui/icmpanel.cc @@ -160,7 +160,7 @@ ICMPanel::ICMPanel () : FoldableToolPanel(this, "icm", M("TP_ICM_LABEL")), iunch wnames = Gtk::manage (new MyComboBoxText ()); wVBox->pack_start (*wnames, Gtk::PACK_SHRINK); - std::vector wpnames = rtengine::ICCStore::getWorkingProfiles(); + std::vector wpnames = rtengine::ICCStore::getInstance()->getWorkingProfiles(); for (size_t i = 0; i < wpnames.size(); i++) { wnames->append (wpnames[i]); From aecbc2cf2411c7a2258c394a9962ee477bfca149 Mon Sep 17 00:00:00 2001 From: Alberto Griggio Date: Fri, 23 Mar 2018 14:37:57 +0100 Subject: [PATCH 2/2] fixed compilation problem --- rtengine/rt_math.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/rtengine/rt_math.h b/rtengine/rt_math.h index dbf54ace5..a1dc11d44 100644 --- a/rtengine/rt_math.h +++ b/rtengine/rt_math.h @@ -138,8 +138,6 @@ constexpr std::uint8_t uint16ToUint8Rounded(std::uint16_t i) return ((i + 128) - ((i + 128) >> 8)) >> 8; } -} - template bool invertMatrix(const std::array, 3> &in, std::array, 3> &out) @@ -201,3 +199,6 @@ std::array dotProduct(const std::array, 3> &a, const std: return res; } + +} +