From e635030650ed54b0c2232e8d5b986db2988809f7 Mon Sep 17 00:00:00 2001 From: rom9 <4711834+rom9@users.noreply.github.com> Date: Sat, 21 Nov 2020 13:29:47 +0100 Subject: [PATCH] FilmNegative: non-raw file support and better accuracy (#5798) * Added support for non-raw files to the film negative tool. The tool is now under the "Color" tab. Moved the entire filmneg code downstream, right after input profile conversion. Usage changes a bit: White Balance must be set to the _backlight_ color temperature. Added two more sliders to color-balance the picture after negative inversion. Legacy inversion method kept for compatibility with processing profiles saved by RT v5.7 or 5.8 (only if Film Negative was enabled). Should be removed in a future version. There is still an issue with DCP profiles that need the look table to be active to work properly. Using a simple matrix input profile (or just camera standard) is recommended for now. * The user can now choose to perform inversion before or after input colorspace conversion. Seamless backwards compatibility with previous processing profiles; upgrading from a previous version now gives an (almost) identical output as before. Generalised the concept of film base values: the processing profile now contains a pair of RGB triplets, "reference input" and "reference output", which makes it much more straightforward to compute the output multipliers. * Added support for `RGB` data type to putToKeyFile, removed the now unused `RGB::toVector()` method. Some cleanup. * Spot balance picker now stays active indefinitely. Can be disabled by right-clicking, too. * Removed film negative from `filterRawRefresh` condition, since the new version does not require raw rendering anymore. * The Output Level slider is now exponential, so it should feel more familiar to use (similar to the exposure compensation slider). * Removed old `RedBase`, `GreenBase`, `BlueBase` keys from the PP3 file after params upgrade. Keys are only kept if the file only undergoes batch edit (hence no params upgrade was done). * Made the balance sliders exponential and centered at zero. Now they should be a bit smoother and possibly more user-friendly. * Changed adjusters' step to more useful values, they were too fine-grained and hard to adjust using the +/- spinbutton. Increased max ratio value from 3 to 5, as i found an old negative needing a very high blue channel exponent. * Added an initial processing profile for film negative inversion, to be provided as a bundled profile. * Removed Output Level and balance sliders when in batch mode: since the effect of these sliders is dependent on the reference input values, those values needed to be copied as well. And, touching any of the sliders needed to flag all three as dirty. All this felt more confusing than useful. It should be sufficient (and clearer) to copy/paste the params, and then fine-tune the balance on individual pictures when needed. * Set bayer demosaic method to RCD in the bundled "Film Negative" processing profile. RCD seems to play a bit better with Capture Sharpening in the presence of film grain, compared to Amaze. This will favor new users starting with all-defaults settings, hence having Capture Sharpening enabled by default. * Removed incorrect "contrast" term from the "Reference exponent" label. This parameter adjusts the image _gamma_, not its contrast. --- rtdata/languages/default | 23 +- rtdata/profiles/Film Negative.pp3 | 35 ++ rtengine/CMakeLists.txt | 1 - rtengine/dcrop.cc | 8 + rtengine/filmnegativeproc.cc | 987 ++++++++++++++++-------------- rtengine/filmnegativethumb.cc | 237 ------- rtengine/imagesource.h | 4 +- rtengine/improccoordinator.cc | 74 +-- rtengine/improccoordinator.h | 3 +- rtengine/improcfun.h | 7 + rtengine/procparams.cc | 149 ++++- rtengine/procparams.h | 26 +- rtengine/rawimagesource.cc | 50 ++ rtengine/rawimagesource.h | 4 +- rtengine/rtengine.h | 7 +- rtengine/rtthumbnail.cc | 35 +- rtengine/rtthumbnail.h | 1 + rtengine/simpleprocess.cc | 28 +- rtengine/stdimagesource.h | 1 + rtgui/filmnegative.cc | 466 +++++++++++--- rtgui/filmnegative.h | 47 +- rtgui/paramsedited.cc | 29 +- rtgui/paramsedited.h | 4 +- rtgui/partialpastedlg.cc | 10 +- rtgui/toolpanelcoord.cc | 24 +- rtgui/toolpanelcoord.h | 3 +- 26 files changed, 1327 insertions(+), 936 deletions(-) create mode 100644 rtdata/profiles/Film Negative.pp3 delete mode 100644 rtengine/filmnegativethumb.cc diff --git a/rtdata/languages/default b/rtdata/languages/default index e13937a10..418f42288 100644 --- a/rtdata/languages/default +++ b/rtdata/languages/default @@ -1257,8 +1257,10 @@ HISTORY_MSG_DEHAZE_STRENGTH;Dehaze - Strength HISTORY_MSG_DUALDEMOSAIC_AUTO_CONTRAST;Dual demosaic - Auto threshold HISTORY_MSG_DUALDEMOSAIC_CONTRAST;Dual demosaic - Contrast threshold HISTORY_MSG_EDGEFFECT;Edge Attenuation response +HISTORY_MSG_FILMNEGATIVE_BALANCE;FN - Reference output +HISTORY_MSG_FILMNEGATIVE_COLORSPACE;Film negative color space HISTORY_MSG_FILMNEGATIVE_ENABLED;Film Negative -HISTORY_MSG_FILMNEGATIVE_FILMBASE;Film base color +HISTORY_MSG_FILMNEGATIVE_REF_SPOT;FN - Reference input HISTORY_MSG_FILMNEGATIVE_VALUES;Film negative values HISTORY_MSG_HISTMATCHING;Auto-matched tone curve HISTORY_MSG_ICM_OUTPUT_PRIMARIES;Output - Primaries @@ -1559,7 +1561,7 @@ PARTIALPASTE_EQUALIZER;Wavelet levels PARTIALPASTE_EVERYTHING;Everything PARTIALPASTE_EXIFCHANGES;Exif PARTIALPASTE_EXPOSURE;Exposure -PARTIALPASTE_FILMNEGATIVE;Film Negative +PARTIALPASTE_FILMNEGATIVE;Film negative PARTIALPASTE_FILMSIMULATION;Film simulation PARTIALPASTE_FLATFIELDAUTOSELECT;Flat-field auto-selection PARTIALPASTE_FLATFIELDBLURRADIUS;Flat-field blur radius @@ -2252,12 +2254,19 @@ TP_EXPOSURE_TCMODE_WEIGHTEDSTD;Weighted Standard TP_EXPOS_BLACKPOINT_LABEL;Raw Black Points TP_EXPOS_WHITEPOINT_LABEL;Raw White Points TP_FILMNEGATIVE_BLUE;Blue ratio -TP_FILMNEGATIVE_FILMBASE_PICK;Pick film base color -TP_FILMNEGATIVE_FILMBASE_TOOLTIP;Pick a spot of unexposed film (eg. the border between frames), to get the actual film base color values, and save them in the processing profile.\nThis makes it easy to get a more consistent color balance when batch-processing multiple pictures from the same roll.\nAlso use this when the converted image is extremely dark, bright, or color-unbalanced. -TP_FILMNEGATIVE_FILMBASE_VALUES;Film base RGB: -TP_FILMNEGATIVE_GREEN;Reference exponent (contrast) -TP_FILMNEGATIVE_GUESS_TOOLTIP;Automatically set the red and blue ratios by picking two patches which had a neutral hue (no color) in the original scene. The patches should differ in brightness. Set the white balance afterwards. +TP_FILMNEGATIVE_BLUEBALANCE;Cool/Warm +TP_FILMNEGATIVE_COLORSPACE;Inversion color space: +TP_FILMNEGATIVE_COLORSPACE_INPUT;Input color space +TP_FILMNEGATIVE_COLORSPACE_TOOLTIP;Select the color space used to perform the negative inversion:\nInput color space : perform inversion before the input profile is applied, as in the previous versions of RT.\nWorking color space : perform inversion after input profile, using the currently selected working profile. +TP_FILMNEGATIVE_COLORSPACE_WORKING;Working color space +TP_FILMNEGATIVE_REF_LABEL;Input RGB: %1 +TP_FILMNEGATIVE_REF_PICK;Pick white balance spot +TP_FILMNEGATIVE_REF_TOOLTIP;Pick a gray patch for white-balancing the output, positive image. +TP_FILMNEGATIVE_GREEN;Reference exponent +TP_FILMNEGATIVE_GREENBALANCE;Magenta/Green +TP_FILMNEGATIVE_GUESS_TOOLTIP;Automatically set the red and blue ratios by picking two patches which had a neutral hue (no color) in the original scene. The patches should differ in brightness. TP_FILMNEGATIVE_LABEL;Film Negative +TP_FILMNEGATIVE_OUT_LEVEL;Output level TP_FILMNEGATIVE_PICK;Pick neutral spots TP_FILMNEGATIVE_RED;Red ratio TP_FILMSIMULATION_LABEL;Film Simulation diff --git a/rtdata/profiles/Film Negative.pp3 b/rtdata/profiles/Film Negative.pp3 new file mode 100644 index 000000000..c298a6ea9 --- /dev/null +++ b/rtdata/profiles/Film Negative.pp3 @@ -0,0 +1,35 @@ +[Exposure] +Auto=false +Compensation=0 +HistogramMatching=false +CurveFromHistogramMatching=false +ClampOOG=false +CurveMode=Standard +CurveMode2=Standard +Curve=1;0;0;0.88544601940051371;1; +Curve2=1;0;0;0.0397505754145333;0.020171771436200074;0.54669745433149319;0.69419974733677647;1;1; + +[HLRecovery] +Enabled=false +Method=Blend + +[Color Management] +InputProfile=(camera) +ToneCurve=false +ApplyLookTable=false +ApplyHueSatMap=false +WorkingProfile=Rec2020 +WorkingTRC=none + +[RAW Bayer] +Method=rcd + +[Film Negative] +Enabled=true +RedRatio=1.360 +GreenExponent=1.5 +BlueRatio=0.86 +ColorSpace=1 +RefInput=0;0;0; +RefOutput=0;0;0; +BackCompat=0 diff --git a/rtengine/CMakeLists.txt b/rtengine/CMakeLists.txt index 543cd4841..cb96daa03 100644 --- a/rtengine/CMakeLists.txt +++ b/rtengine/CMakeLists.txt @@ -94,7 +94,6 @@ set(RTENGINESOURCEFILES fast_demo.cc ffmanager.cc filmnegativeproc.cc - filmnegativethumb.cc flatcurves.cc FTblockDN.cc gamutwarning.cc diff --git a/rtengine/dcrop.cc b/rtengine/dcrop.cc index e5eb18330..64e2d6e60 100644 --- a/rtengine/dcrop.cc +++ b/rtengine/dcrop.cc @@ -669,8 +669,16 @@ void Crop::update(int todo) } } + if (params.filmNegative.enabled && params.filmNegative.colorSpace == FilmNegativeParams::ColorSpace::INPUT) { + parent->ipf.filmNegativeProcess(baseCrop, baseCrop, params.filmNegative); + } + parent->imgsrc->convertColorSpace(origCrop, params.icm, parent->currWB); + if (params.filmNegative.enabled && params.filmNegative.colorSpace != FilmNegativeParams::ColorSpace::INPUT) { + parent->ipf.filmNegativeProcess(baseCrop, baseCrop, params.filmNegative); + } + delete [] min_r; delete [] min_b; delete [] lumL; diff --git a/rtengine/filmnegativeproc.cc b/rtengine/filmnegativeproc.cc index 30d0a052d..ae1813db9 100644 --- a/rtengine/filmnegativeproc.cc +++ b/rtengine/filmnegativeproc.cc @@ -21,550 +21,647 @@ #include "rawimage.h" #include "rawimagesource.h" +#include "improcfun.h" +#include "improccoordinator.h" +#include "imagefloat.h" #include "coord.h" -#include "mytime.h" #include "opthelper.h" #include "pixelsmap.h" #include "procparams.h" #include "rt_algo.h" #include "rtengine.h" +#include "rtthumbnail.h" #include "sleef.h" //#define BENCHMARK -#include "StopWatch.h" +//#include "StopWatch.h" +#include "iccstore.h" +#include "rt_math.h" +#include "color.h" + namespace { -using rtengine::ST_BAYER; -using rtengine::ST_FUJI_XTRANS; using rtengine::settings; +using rtengine::Coord2D; +using rtengine::ColorTemp; +using rtengine::findMinMaxPercentile; +using rtengine::ImageSource; +using rtengine::Imagefloat; +using rtengine::procparams::FilmNegativeParams; +using rtengine::procparams::ColorManagementParams; +using rtengine::procparams::RAWParams; +using rtengine::ICCStore; +using rtengine::MAXVALF; +using rtengine::CLIP; +using rtengine::TMatrix; +using rtengine::Color; +using RGB = rtengine::procparams::FilmNegativeParams::RGB; -bool channelsAvg( - const rtengine::RawImage* ri, - int width, - int height, - const float* cblacksom, - rtengine::Coord spotPos, - int spotSize, - std::array& avgs -) +Coord2D translateCoord(const rtengine::ImProcFunctions& ipf, int fw, int fh, int x, int y) { - avgs = {}; // Channel averages - if (ri->getSensorType() != ST_BAYER && ri->getSensorType() != ST_FUJI_XTRANS) { - return false; - } + const std::vector points = {Coord2D(x, y)}; + + std::vector red; + std::vector green; + std::vector blue; + ipf.transCoord(fw, fh, points, red, green, blue); + + return green[0]; +} + + +void getSpotAvgMax(ImageSource *imgsrc, ColorTemp currWB, const std::unique_ptr ¶ms, + Coord2D p, int tr, int spotSize, RGB &avg, RGB &max) +{ + int x1 = MAX(0, (int)p.x - spotSize / 2); + int y1 = MAX(0, (int)p.y - spotSize / 2); + PreviewProps pp(x1, y1, spotSize, spotSize, 1); if (settings->verbose) { - printf("Spot coord: x=%d y=%d\n", spotPos.x, spotPos.y); + printf("Spot: %d,%d %d,%d\n", x1, y1, x1 + spotSize / 2, y1 + spotSize / 2); } - const int half_spot_size = spotSize / 2; + rtengine::Imagefloat spotImg(spotSize, spotSize); + imgsrc->getImage(currWB, tr, &spotImg, pp, params->toneCurve, params->raw); - const int& x1 = spotPos.x - half_spot_size; - const int& x2 = spotPos.x + half_spot_size; - const int& y1 = spotPos.y - half_spot_size; - const int& y2 = spotPos.y + half_spot_size; + auto avgMax = [spotSize, &spotImg](RGB & avg, RGB & max) -> void { + avg = {}; + max = {}; - if (x1 < 0 || x2 > width || y1 < 0 || y2 > height) { - return false; // Spot goes outside bounds, bail out. - } + for (int i = 0; i < spotSize; ++i) + { + for (int j = 0; j < spotSize; ++j) { - std::array pxCount = {}; // Per-channel sample counts + float r = spotImg.r(i, j); + float g = spotImg.g(i, j); + float b = spotImg.b(i, j); - for (int c = x1; c < x2; ++c) { - for (int r = y1; r < y2; ++r) { - const int ch = ri->getSensorType() == ST_BAYER ? ri->FC(r, c) : ri->XTRANSFC(r, c); - - ++pxCount[ch]; - - // Sample the original unprocessed values from RawImage, subtracting black levels. - // Scaling is irrelevant, as we are only interested in the ratio between two spots. - avgs[ch] += ri->data[r][c] - cblacksom[ch]; - } - } - - for (int ch = 0; ch < 3; ++ch) { - avgs[ch] /= pxCount[ch]; - } - - return true; -} - -void calcMedians( - const rtengine::RawImage* ri, - float** data, - int x1, int y1, int x2, int y2, - std::array& meds -) -{ - - MyTime t1, t2, t3; - t1.set(); - - // Channel vectors to calculate medians - std::array, 3> cvs; - - // Sample one every 5 pixels, and push the value in the appropriate channel vector. - // Choose an odd step, not a multiple of the CFA size, to get a chance to visit each channel. - if (ri->getSensorType() == ST_BAYER) { - for (int row = y1; row < y2; row += 5) { - const int c0 = ri->FC(row, x1 + 0); - const int c1 = ri->FC(row, x1 + 5); - int col = x1; - - for (; col < x2 - 5; col += 10) { - cvs[c0].push_back(data[row][col]); - cvs[c1].push_back(data[row][col + 5]); - } - - if (col < x2) { - cvs[c0].push_back(data[row][col]); + avg.r += r; + avg.g += g; + avg.b += b; + max.r = MAX(max.r, r); + max.g = MAX(max.g, g); + max.b = MAX(max.b, b); } } - } else if (ri->getSensorType() == ST_FUJI_XTRANS) { - for (int row = y1; row < y2; row += 5) { - const std::array cs = { - ri->XTRANSFC(row, x1 + 0), - ri->XTRANSFC(row, x1 + 5), - ri->XTRANSFC(row, x1 + 10), - ri->XTRANSFC(row, x1 + 15), - ri->XTRANSFC(row, x1 + 20), - ri->XTRANSFC(row, x1 + 25) - }; - int col = x1; - for (; col < x2 - 25; col += 30) { - for (int c = 0; c < 6; ++c) { - cvs[cs[c]].push_back(data[row][col + c * 5]); - } - } - - for (int c = 0; col < x2; col += 5, ++c) { - cvs[cs[c]].push_back(data[row][col]); - } - } - } - - t2.set(); - - if (settings->verbose) { - printf("Median vector fill loop time us: %d\n", t2.etime(t1)); - } - - t2.set(); - - for (int c = 0; c < 3; ++c) { - // Find median values for each channel - if (!cvs[c].empty()) { - rtengine::findMinMaxPercentile(cvs[c].data(), cvs[c].size(), 0.5f, meds[c], 0.5f, meds[c], true); - } - } - - t3.set(); - - if (settings->verbose) { - printf("Sample count: R=%zu, G=%zu, B=%zu\n", cvs[0].size(), cvs[1].size(), cvs[2].size()); - printf("Median calc time us: %d\n", t3.etime(t2)); - } - -} - -std::array calcWBMults( - const rtengine::ColorTemp& wb, - const rtengine::ImageMatrices& imatrices, - const rtengine::RawImage *ri, - const float ref_pre_mul[4]) -{ - std::array wb_mul; - double r, g, b; - wb.getMultipliers(r, g, b); - wb_mul[0] = imatrices.cam_rgb[0][0] * r + imatrices.cam_rgb[0][1] * g + imatrices.cam_rgb[0][2] * b; - wb_mul[1] = imatrices.cam_rgb[1][0] * r + imatrices.cam_rgb[1][1] * g + imatrices.cam_rgb[1][2] * b; - wb_mul[2] = imatrices.cam_rgb[2][0] * r + imatrices.cam_rgb[2][1] * g + imatrices.cam_rgb[2][2] * b; - - for (int c = 0; c < 3; ++c) { - wb_mul[c] = ri->get_pre_mul(c) / wb_mul[c] / ref_pre_mul[c]; - } - - // Normalize max channel gain to 1.0 - float mg = rtengine::max(wb_mul[0], wb_mul[1], wb_mul[2]); - - for (int c = 0; c < 3; ++c) { - wb_mul[c] /= mg; - } - - return wb_mul; -} - -} - -bool rtengine::RawImageSource::getFilmNegativeExponents(Coord2D spotA, Coord2D spotB, int tran, const procparams::FilmNegativeParams ¤tParams, std::array& newExps) -{ - newExps = { - static_cast(currentParams.redRatio * currentParams.greenExp), - static_cast(currentParams.greenExp), - static_cast(currentParams.blueRatio * currentParams.greenExp) + avg.r /= (spotSize * spotSize); + avg.g /= (spotSize * spotSize); + avg.b /= (spotSize * spotSize); }; - constexpr int spotSize = 32; // TODO: Make this configurable? + if (params->filmNegative.colorSpace == rtengine::FilmNegativeParams::ColorSpace::INPUT) { + avgMax(avg, max); + } else { + // Convert spot image to current working space + imgsrc->convertColorSpace(&spotImg, params->icm, currWB); - Coord spot; - std::array clearVals; - std::array denseVals; + avgMax(avg, max); - // Get channel averages in the two spots, sampling from the original ri->data buffer. - // NOTE: rawData values might be affected by CA correction, FlatField, etc, so: - // rawData[y][x] == (ri->data[y][x] - cblacksom[c]) * scale_mul[c] - // is not always true. To calculate exponents on the exact values, we should keep - // a copy of the rawData buffer after preprocessing. Worth the memory waste? - - // Sample first spot - transformPosition(spotA.x, spotA.y, tran, spot.x, spot.y); - - if (!channelsAvg(ri, W, H, cblacksom, spot, spotSize, clearVals)) { - return false; + // TODO handle custom color profile ! } - // Sample second spot - transformPosition(spotB.x, spotB.y, tran, spot.x, spot.y); - - if (!channelsAvg(ri, W, H, cblacksom, spot, spotSize, denseVals)) { - return false; - } - - // Detect which one is the dense spot, based on green channel - if (clearVals[1] < denseVals[1]) { - std::swap(clearVals, denseVals); - } + // Clip away zeroes or negative numbers, you never know + avg.r = MAX(avg.r, 1.f); + avg.g = MAX(avg.g, 1.f); + avg.b = MAX(avg.b, 1.f); if (settings->verbose) { - printf("Clear film values: R=%g G=%g B=%g\n", static_cast(clearVals[0]), static_cast(clearVals[1]), static_cast(clearVals[2])); - printf("Dense film values: R=%g G=%g B=%g\n", static_cast(denseVals[0]), static_cast(denseVals[1]), static_cast(denseVals[2])); + printf("Average Spot RGB: %f,%f,%f\n", avg.r, avg.g, avg.b); } +} - const float denseGreenRatio = clearVals[1] / denseVals[1]; - // Calculate logarithms in arbitrary base - const auto logBase = - [](float base, float num) -> float - { - return std::log(num) / std::log(base); - }; +void calcMedians( + const rtengine::Imagefloat* input, + int x1, int y1, int x2, int y2, + float &rmed, float &gmed, float &bmed +) +{ + using rtengine::findMinMaxPercentile; - // Calculate exponents for each channel, based on the ratio between the bright and dark values, - // compared to the ratio in the reference channel (green) - for (int ch = 0; ch < 3; ++ch) { - if (ch == 1) { - newExps[ch] = 1.f; // Green is the reference channel - } else { - newExps[ch] = rtengine::LIM(logBase(clearVals[ch] / denseVals[ch], denseGreenRatio), 0.3f, 4.f); + // Channel vectors to calculate medians + std::vector rv, gv, bv; + + const int sz = (x2 - x1) * (y2 - y1); + rv.reserve(sz); + gv.reserve(sz); + bv.reserve(sz); + + + for (int ii = y1; ii < y2; ii ++) { + for (int jj = x1; jj < x2; jj ++) { + rv.push_back(input->r(ii, jj)); + gv.push_back(input->g(ii, jj)); + bv.push_back(input->b(ii, jj)); } } - if (settings->verbose) { - printf("New exponents: R=%g G=%g B=%g\n", static_cast(newExps[0]), static_cast(newExps[1]), static_cast(newExps[2])); - } - - return true; + // Calculate channel medians of the specified area + findMinMaxPercentile(rv.data(), rv.size(), 0.5f, rmed, 0.5f, rmed, true); + findMinMaxPercentile(gv.data(), gv.size(), 0.5f, gmed, 0.5f, gmed, true); + findMinMaxPercentile(bv.data(), bv.size(), 0.5f, bmed, 0.5f, bmed, true); } -bool rtengine::RawImageSource::getRawSpotValues(Coord2D spotCoord, int spotSize, int tran, const procparams::FilmNegativeParams ¶ms, std::array& rawValues) + +RGB getMedians(const rtengine::Imagefloat* input, int borderPercent) { - Coord spot; - transformPosition(spotCoord.x, spotCoord.y, tran, spot.x, spot.y); + float rmed, gmed, bmed; + // Cut 20% border from medians calculation. It will probably contain outlier values + // from the film holder, which will bias the median result. + const int bW = input->getWidth() * borderPercent / 100; + const int bH = input->getHeight() * borderPercent / 100; + calcMedians(input, bW, bH, + input->getWidth() - bW, input->getHeight() - bH, + rmed, gmed, bmed); if (settings->verbose) { - printf("Transformed coords: %d,%d\n", spot.x, spot.y); + printf("Channel medians: R=%g, G=%g, B=%g\n", rmed, gmed, bmed); } - if (spotSize < 4) { + return { rmed, gmed, bmed }; +} + +/* +// TODO not needed for now +void convertColorSpace(Imagefloat* input, const TMatrix &src2xyz, const TMatrix &xyz2dest) +{ + +#ifdef _OPENMP + #pragma omp parallel for +#endif + + for (int i = 0; i < input->getHeight(); i++) { + for (int j = 0; j < input->getWidth(); j++) { + + float newr = input->r(i, j); + float newg = input->g(i, j); + float newb = input->b(i, j); + + float x, y, z; + Color::rgbxyz (newr, newg, newb, x, y, z, src2xyz); + Color::xyz2rgb (x, y, z, newr, newg, newb, xyz2dest); + + input->r(i, j) = newr; + input->g(i, j) = newg; + input->b(i, j) = newb; + + } + } +} +*/ + + +/** + * Perform actual film negative inversion process. + * Returns true if the input and output reference values are not set in params; refIn/refOut will be updated with median-based estimates. + * Otherwise, use provided values in params and return false + */ +bool doProcess(Imagefloat *input, Imagefloat *output, + const FilmNegativeParams ¶ms, const ColorManagementParams &icmParams, + RGB &refIn, RGB &refOut) +{ + bool refsUpdated = false; + + float rexp = -(params.greenExp * params.redRatio); + float gexp = -params.greenExp; + float bexp = -(params.greenExp * params.blueRatio); + + // In case we are processing a thumbnail, reference values might not be set in params, + // so make an estimate on the fly, using channel medians + if (refIn.g <= 0.f) { + // Calc medians, 20% border cut + refIn = getMedians(input, 20); + refsUpdated = true; + } else { + refIn = params.refInput; + } + + if (refOut.g <= 0.f) { + // Median will correspond to gray, 1/24th of max in the output + refOut = { MAXVALF / 24.f, MAXVALF / 24.f, MAXVALF / 24.f }; + refsUpdated = true; + } else { + refOut = params.refOutput; + } + + // Apply channel exponents to reference input values, and compute suitable multipliers + // in order to reach reference output values. + float rmult = refOut.r / pow_F(rtengine::max(refIn.r, 1.f), rexp); + float gmult = refOut.g / pow_F(rtengine::max(refIn.g, 1.f), gexp); + float bmult = refOut.b / pow_F(rtengine::max(refIn.b, 1.f), bexp); + + +#ifdef __SSE2__ + const vfloat clipv = F2V(MAXVALF); + const vfloat rexpv = F2V(rexp); + const vfloat gexpv = F2V(gexp); + const vfloat bexpv = F2V(bexp); + const vfloat rmultv = F2V(rmult); + const vfloat gmultv = F2V(gmult); + const vfloat bmultv = F2V(bmult); +#endif + + const int rheight = input->getHeight(); + const int rwidth = input->getWidth(); + + for (int i = 0; i < rheight; i++) { + float *rlinein = input->r(i); + float *glinein = input->g(i); + float *blinein = input->b(i); + float *rlineout = output->r(i); + float *glineout = output->g(i); + float *blineout = output->b(i); + int j = 0; +#ifdef __SSE2__ + + for (; j < rwidth - 3; j += 4) { + STVFU(rlineout[j], vminf(rmultv * pow_F(LVFU(rlinein[j]), rexpv), clipv)); + STVFU(glineout[j], vminf(gmultv * pow_F(LVFU(glinein[j]), gexpv), clipv)); + STVFU(blineout[j], vminf(bmultv * pow_F(LVFU(blinein[j]), bexpv), clipv)); + } + +#endif + + for (; j < rwidth; ++j) { + rlineout[j] = CLIP(rmult * pow_F(rlinein[j], rexp)); + glineout[j] = CLIP(gmult * pow_F(glinein[j], gexp)); + blineout[j] = CLIP(bmult * pow_F(blinein[j], bexp)); + } + } + + return refsUpdated; +} + + +} + + + +bool rtengine::ImProcFunctions::filmNegativeProcess( + Imagefloat *input, Imagefloat *output, FilmNegativeParams &fnp, + const RAWParams &rawParams, const ImageSource* imgsrc, const ColorTemp &currWB) +{ + //BENCHFUNMICRO + + if (!fnp.enabled) { return false; } - // Calculate averages of raw unscaled channels - if (!channelsAvg(ri, W, H, cblacksom, spot, spotSize, rawValues)) { - return false; + bool paramsUpdated = false; + + RGB &refIn = fnp.refInput; + RGB &refOut = fnp.refOutput; + + // If we're opening a profile from an older version, apply the proper multiplier + // compensations to make processing backwards compatible. + + if (fnp.backCompat == FilmNegativeParams::BackCompat::V1) { + // Calc medians, no border cut, compensate currWB in+out + refIn = getMedians(input, 0); + refOut = { MAXVALF / 24.f, MAXVALF / 24.f, MAXVALF / 24.f }; + + std::array scale_mul = { 1.f, 1.f, 1.f, 1.f }; + float autoGainComp, rm, gm, bm; + imgsrc->getWBMults(currWB, params->raw, scale_mul, autoGainComp, rm, gm, bm); + + refOut.r *= rm; + refOut.g *= gm; + refOut.b *= bm; + + paramsUpdated = true; + + } else if (fnp.backCompat == FilmNegativeParams::BackCompat::V2) { + + std::array scale_mul = { 1.f, 1.f, 1.f, 1.f }; + float autoGainComp, rm, gm, bm; + imgsrc->getWBMults(currWB, params->raw, scale_mul, autoGainComp, rm, gm, bm); + + float rm2, gm2, bm2; + imgsrc->getWBMults(rtengine::ColorTemp(3500., 1., 1., "Custom"), params->raw, scale_mul, autoGainComp, rm2, gm2, bm2); + float mg = rtengine::max(rm2, gm2, bm2); + rm2 /= mg; + gm2 /= mg; + bm2 /= mg; + + if (fnp.refInput.g == 0.f) { + // Calc medians, 20% border cut + refIn = getMedians(input, 20); + refOut = { MAXVALF / 24.f, MAXVALF / 24.f, MAXVALF / 24.f }; + } else if (fnp.refInput.g > 0.f) { + // Calc refInput + refOutput from base levels, compensate currWB in, 3500 out + refIn = fnp.refInput; + refIn.r *= rm * scale_mul[0]; + refIn.g *= gm * scale_mul[1]; + refIn.b *= bm * scale_mul[2]; + refOut = { MAXVALF / 512.f, MAXVALF / 512.f, MAXVALF / 512.f }; + } + + refOut.r *= rm * autoGainComp / rm2; + refOut.g *= gm * autoGainComp / gm2; + refOut.b *= bm * autoGainComp / bm2; + + paramsUpdated = true; + } - if (settings->verbose) { - printf("Raw spot values: R=%g, G=%g, B=%g\n", rawValues[0], rawValues[1], rawValues[2]); + if (settings->verbose && fnp.backCompat != FilmNegativeParams::BackCompat::CURRENT) { + printf("Upgraded from V%d - refIn: R=%g G=%g B=%g refOut: R=%g G=%g B=%g\n", + (int)fnp.backCompat, + static_cast(refIn.r), static_cast(refIn.g), static_cast(refIn.b), + static_cast(refOut.r), static_cast(refOut.g), static_cast(refOut.b)); } - return true; + // FilmNeg params are now upgraded to the latest version + fnp.backCompat = FilmNegativeParams::BackCompat::CURRENT; + + // Perform actual inversion. Return true if reference values are computed from medians + paramsUpdated |= doProcess(input, output, fnp, this->params->icm, refIn, refOut); + + return paramsUpdated; + } -void rtengine::RawImageSource::filmNegativeProcess(const procparams::FilmNegativeParams ¶ms, std::array& filmBaseValues) +void rtengine::ImProcFunctions::filmNegativeProcess(rtengine::Imagefloat *input, rtengine::Imagefloat *output, + const procparams::FilmNegativeParams ¶ms) { -// BENCHFUNMICRO + //BENCHFUNMICRO if (!params.enabled) { return; } - // Exponents are expressed as positive in the parameters, so negate them in order - // to get the reciprocals. - const std::array exps = { - static_cast(-params.redRatio * params.greenExp), - static_cast(-params.greenExp), - static_cast(-params.blueRatio * params.greenExp) - }; + RGB refIn = params.refInput, refOut = params.refOutput; - constexpr float MAX_OUT_VALUE = 65000.f; + doProcess(input, output, params, this->params->icm, refIn, refOut); - // Get multipliers for a known, fixed WB setting, that will be the starting point - // for balancing the converted image. - const std::array wb_mul = calcWBMults( - ColorTemp(3500., 1., 1., "Custom"), imatrices, ri, ref_pre_mul); +} - if (rtengine::settings->verbose) { - printf("Fixed WB mults: %g %g %g\n", wb_mul[0], wb_mul[1], wb_mul[2]); - } +bool rtengine::ImProcCoordinator::getFilmNegativeSpot(int x, int y, const int spotSize, RGB &refInput, RGB &refOutput) +{ + MyMutex::MyLock lock(mProcessing); - // Compensation factor to restore the non-autoWB initialGain (see RawImageSource::load) - const float autoGainComp = camInitialGain / initialGain; + const int tr = getCoarseBitMask(params->coarse); - std::array mults; // Channel normalization multipliers + const Coord2D p = translateCoord(ipf, fw, fh, x, y); - // If film base values are set in params, use those - if (filmBaseValues[0] <= 0.f) { - // ...otherwise, the film negative tool might have just been enabled on this image, - // without any previous setting. So, estimate film base values from channel medians + // Get the average channel values from the sampled spot + RGB avg, max; + getSpotAvgMax(imgsrc, currWB, params, p, tr, spotSize, avg, max); - std::array medians; + float rexp = -(params->filmNegative.greenExp * params->filmNegative.redRatio); + float gexp = -params->filmNegative.greenExp; + float bexp = -(params->filmNegative.greenExp * params->filmNegative.blueRatio); - // Special value for backwards compatibility with profiles saved by RT 5.7 - const bool oldChannelScaling = filmBaseValues[0] == -1.f; + float rmult = params->filmNegative.refOutput.r / pow_F(rtengine::max(params->filmNegative.refInput.r, 1.f), rexp); + float gmult = params->filmNegative.refOutput.g / pow_F(rtengine::max(params->filmNegative.refInput.g, 1.f), gexp); + float bmult = params->filmNegative.refOutput.b / pow_F(rtengine::max(params->filmNegative.refInput.b, 1.f), bexp); - // If using the old channel scaling method, get medians from the whole current image, - // reading values from the already-scaled rawData buffer. - if (oldChannelScaling) { - calcMedians(ri, rawData, 0, 0, W, H, medians); - } else { - // Cut 20% border from medians calculation. It will probably contain outlier values - // from the film holder, which will bias the median result. - const int bW = W * 20 / 100; - const int bH = H * 20 / 100; - calcMedians(ri, rawData, bW, bH, W - bW, H - bH, medians); - } + refInput = avg; - // Un-scale rawData medians - for (int c = 0; c < 3; ++c) { - medians[c] /= scale_mul[c]; - } + refOutput.r = rmult * pow_F(avg.r, rexp); + refOutput.g = gmult * pow_F(avg.g, gexp); + refOutput.b = bmult * pow_F(avg.b, bexp); - if (settings->verbose) { - printf("Channel medians: R=%g, G=%g, B=%g\n", medians[0], medians[1], medians[2]); - } - - for (int c = 0; c < 3; ++c) { - - // Estimate film base values, so that in the output data, each channel - // median will correspond to 1/24th of MAX. - filmBaseValues[c] = pow_F(24.f / 512.f, 1.f / exps[c]) * medians[c]; - - if (oldChannelScaling) { - // If using the old channel scaling method, apply WB multipliers here to undo their - // effect later, since fixed wb compensation was not used in previous version. - // Also, undo the effect of the autoGainComp factor (see below). - filmBaseValues[c] /= pow_F((wb_mul[c] / autoGainComp), 1.f / exps[c]);// / autoGainComp; - } - - } - } + return true; +} - // Calculate multipliers based on previously obtained film base input values. - // Apply current scaling coefficients to raw, unscaled base values. - std::array fb = { - filmBaseValues[0] * scale_mul[0], - filmBaseValues[1] * scale_mul[1], - filmBaseValues[2] * scale_mul[2] - }; + + + + +// ---------- >>> legacy mode >>> --------------- + + +// For backwards compatibility with profiles saved by RT 5.7 - 5.8 +void rtengine::Thumbnail::processFilmNegative( + const procparams::ProcParams ¶ms, + const Imagefloat* baseImg, + const int rwidth, const int rheight +) +{ + + // Channel exponents + const float rexp = -params.filmNegative.redRatio * params.filmNegative.greenExp; + const float gexp = -params.filmNegative.greenExp; + const float bexp = -params.filmNegative.blueRatio * params.filmNegative.greenExp; + + const float MAX_OUT_VALUE = 65000.f; + + // Channel medians + float rmed, gmed, bmed; + + // If using the old method, calculate medians on the whole image + calcMedians(baseImg, 0, 0, rwidth, rheight, rmed, gmed, bmed); if (settings->verbose) { - printf("Input film base values: %g %g %g\n", fb[0], fb[1], fb[2]); + printf("FilmNeg legacy V1 :: Thumbnail input channel medians: %g %g %g\n", rmed, gmed, bmed); } - for (int c = 0; c < 3; ++c) { - // Apply channel exponents, to obtain the corresponding base values in the output data - fb[c] = pow_F(rtengine::max(fb[c], 1.f), exps[c]); + // Calculate output medians + rmed = powf(rmed, rexp); + gmed = powf(gmed, gexp); + bmed = powf(bmed, bexp); - // Determine the channel multiplier so that the film base value is 1/512th of max. - mults[c] = (MAX_OUT_VALUE / 512.f) / fb[c]; + // Calculate output multipliers so that the median value is 1/24 of the output range. + float rmult, gmult, bmult; + rmult = (MAX_OUT_VALUE / 24.f) / rmed; + gmult = (MAX_OUT_VALUE / 24.f) / gmed; + bmult = (MAX_OUT_VALUE / 24.f) / bmed; - // Un-apply the fixed WB multipliers, to reverse their effect later in the WB tool. - // This way, the output image will be adapted to this fixed WB setting - mults[c] /= wb_mul[c]; - - // Also compensate for the initialGain difference between the default scaling (forceAutoWB=true) - // and the non-autoWB scaling (see camInitialGain). - // This effectively restores camera scaling multipliers, and gives us stable multipliers - // (not depending on image content). - mults[c] *= autoGainComp; + float rsum = 0.f, gsum = 0.f, bsum = 0.f; + for (int i = 0; i < rheight; i++) { + for (int j = 0; j < rwidth; j++) { + rsum += baseImg->r(i, j); + gsum += baseImg->g(i, j); + bsum += baseImg->b(i, j); + } } + const float ravg = rsum / (rheight * rwidth); + const float gavg = gsum / (rheight * rwidth); + const float bavg = bsum / (rheight * rwidth); + + // Shifting current WB multipliers, based on channel averages. + rmult /= gavg / ravg; + // gmult /= gAvg / gAvg; green chosen as reference channel + bmult /= gavg / bavg; + if (settings->verbose) { - printf("Output film base values: %g %g %g\n", static_cast(fb[0]), static_cast(fb[1]), static_cast(fb[2])); - printf("Computed multipliers: %g %g %g\n", static_cast(mults[0]), static_cast(mults[1]), static_cast(mults[2])); + printf("FilmNeg legacy V1 :: Thumbnail computed multipliers: %g %g %g\n", static_cast(rmult), static_cast(gmult), static_cast(bmult)); } - - constexpr float CLIP_VAL = 65535.f; - - MyTime t1, t2, t3; - - t1.set(); - - if (ri->getSensorType() == ST_BAYER) { #ifdef __SSE2__ - const vfloat onev = F2V(1.f); - const vfloat clipv = F2V(CLIP_VAL); + const vfloat clipv = F2V(MAXVALF); + const vfloat rexpv = F2V(rexp); + const vfloat gexpv = F2V(gexp); + const vfloat bexpv = F2V(bexp); + const vfloat rmultv = F2V(rmult); + const vfloat gmultv = F2V(gmult); + const vfloat bmultv = F2V(bmult); #endif -#ifdef _OPENMP - #pragma omp parallel for schedule(dynamic, 16) -#endif - - for (int row = 0; row < H; ++row) { - int col = 0; - // Avoid trouble with zeroes, minimum pixel value is 1. - const float exps0 = exps[FC(row, col)]; - const float exps1 = exps[FC(row, col + 1)]; - const float mult0 = mults[FC(row, col)]; - const float mult1 = mults[FC(row, col + 1)]; + for (int i = 0; i < rheight; i++) { + float *rline = baseImg->r(i); + float *gline = baseImg->g(i); + float *bline = baseImg->b(i); + int j = 0; #ifdef __SSE2__ - const vfloat expsv = _mm_setr_ps(exps0, exps1, exps0, exps1); - const vfloat multsv = _mm_setr_ps(mult0, mult1, mult0, mult1); - for (; col < W - 3; col += 4) { - STVFU(rawData[row][col], vminf(multsv * pow_F(vmaxf(LVFU(rawData[row][col]), onev), expsv), clipv)); - } - -#endif // __SSE2__ - - for (; col < W - 1; col += 2) { - rawData[row][col] = rtengine::min(mult0 * pow_F(rtengine::max(rawData[row][col], 1.f), exps0), CLIP_VAL); - rawData[row][col + 1] = rtengine::min(mult1 * pow_F(rtengine::max(rawData[row][col + 1], 1.f), exps1), CLIP_VAL); - } - - if (col < W) { - rawData[row][col] = rtengine::min(mult0 * pow_F(rtengine::max(rawData[row][col], 1.f), exps0), CLIP_VAL); - } + for (; j < rwidth - 3; j += 4) { + STVFU(rline[j], vminf(rmultv * pow_F(LVFU(rline[j]), rexpv), clipv)); + STVFU(gline[j], vminf(gmultv * pow_F(LVFU(gline[j]), gexpv), clipv)); + STVFU(bline[j], vminf(bmultv * pow_F(LVFU(bline[j]), bexpv), clipv)); } - } else if (ri->getSensorType() == ST_FUJI_XTRANS) { -#ifdef __SSE2__ - const vfloat onev = F2V(1.f); - const vfloat clipv = F2V(CLIP_VAL); + #endif -#ifdef _OPENMP - #pragma omp parallel for schedule(dynamic, 16) -#endif - - for (int row = 0; row < H; row ++) { - int col = 0; - // Avoid trouble with zeroes, minimum pixel value is 1. - const std::array expsc = { - exps[ri->XTRANSFC(row, 0)], - exps[ri->XTRANSFC(row, 1)], - exps[ri->XTRANSFC(row, 2)], - exps[ri->XTRANSFC(row, 3)], - exps[ri->XTRANSFC(row, 4)], - exps[ri->XTRANSFC(row, 5)] - }; - const std::array multsc = { - mults[ri->XTRANSFC(row, 0)], - mults[ri->XTRANSFC(row, 1)], - mults[ri->XTRANSFC(row, 2)], - mults[ri->XTRANSFC(row, 3)], - mults[ri->XTRANSFC(row, 4)], - mults[ri->XTRANSFC(row, 5)] - }; -#ifdef __SSE2__ - const vfloat expsv0 = _mm_setr_ps(expsc[0], expsc[1], expsc[2], expsc[3]); - const vfloat expsv1 = _mm_setr_ps(expsc[4], expsc[5], expsc[0], expsc[1]); - const vfloat expsv2 = _mm_setr_ps(expsc[2], expsc[3], expsc[4], expsc[5]); - const vfloat multsv0 = _mm_setr_ps(multsc[0], multsc[1], multsc[2], multsc[3]); - const vfloat multsv1 = _mm_setr_ps(multsc[4], multsc[5], multsc[0], multsc[1]); - const vfloat multsv2 = _mm_setr_ps(multsc[2], multsc[3], multsc[4], multsc[5]); - - for (; col < W - 11; col += 12) { - STVFU(rawData[row][col], vminf(multsv0 * pow_F(vmaxf(LVFU(rawData[row][col]), onev), expsv0), clipv)); - STVFU(rawData[row][col + 4], vminf(multsv1 * pow_F(vmaxf(LVFU(rawData[row][col + 4]), onev), expsv1), clipv)); - STVFU(rawData[row][col + 8], vminf(multsv2 * pow_F(vmaxf(LVFU(rawData[row][col + 8]), onev), expsv2), clipv)); - } - -#endif // __SSE2__ - - for (; col < W - 5; col += 6) { - for (int c = 0; c < 6; ++c) { - rawData[row][col + c] = rtengine::min(multsc[c] * pow_F(rtengine::max(rawData[row][col + c], 1.f), expsc[c]), CLIP_VAL); - } - } - - for (int c = 0; col < W; col++, c++) { - rawData[row][col + c] = rtengine::min(multsc[c] * pow_F(rtengine::max(rawData[row][col + c], 1.f), expsc[c]), CLIP_VAL); - } + for (; j < rwidth; ++j) { + rline[j] = CLIP(rmult * pow_F(rline[j], rexp)); + gline[j] = CLIP(gmult * pow_F(gline[j], gexp)); + bline[j] = CLIP(bmult * pow_F(bline[j], bexp)); } } - - t2.set(); - - if (settings->verbose) { - printf("Pow loop time us: %d\n", t2.etime(t1)); - } - - t2.set(); - - PixelsMap bitmapBads(W, H); - - int totBP = 0; // Hold count of bad pixels to correct - - if (ri->getSensorType() == ST_BAYER) { -#ifdef _OPENMP - #pragma omp parallel for reduction(+:totBP) schedule(dynamic,16) -#endif - - for (int i = 0; i < H; ++i) { - for (int j = 0; j < W; ++j) { - if (rawData[i][j] >= MAX_OUT_VALUE) { - bitmapBads.set(j, i); - ++totBP; - } - } - } - - if (totBP > 0) { - interpolateBadPixelsBayer(bitmapBads, rawData); - } - - } else if (ri->getSensorType() == ST_FUJI_XTRANS) { -#ifdef _OPENMP - #pragma omp parallel for reduction(+:totBP) schedule(dynamic,16) -#endif - - for (int i = 0; i < H; ++i) { - for (int j = 0; j < W; ++j) { - if (rawData[i][j] >= MAX_OUT_VALUE) { - bitmapBads.set(j, i); - totBP++; - } - } - } - - if (totBP > 0) { - interpolateBadPixelsXtrans(bitmapBads); - } - } - - t3.set(); - - if (settings->verbose) { - printf("Bad pixels count: %d\n", totBP); - printf("Bad pixels interpolation time us: %d\n", t3.etime(t2)); - } } + + +// For backwards compatibility with intermediate dev version (see filmneg_stable_mults branch) +void rtengine::Thumbnail::processFilmNegativeV2( + const procparams::ProcParams ¶ms, + const Imagefloat* baseImg, + const int rwidth, const int rheight +) +{ + + // Channel exponents + const float rexp = -params.filmNegative.redRatio * params.filmNegative.greenExp; + const float gexp = -params.filmNegative.greenExp; + const float bexp = -params.filmNegative.blueRatio * params.filmNegative.greenExp; + + // Calculate output multipliers + float rmult, gmult, bmult; + + const float MAX_OUT_VALUE = 65000.f; + + // If the film base values are not set in params, estimate multipliers from each channel's median value. + if (params.filmNegative.refInput.r <= 0.f) { + + // Channel medians + float rmed, gmed, bmed; + + // The new method cuts out a 20% border from medians calculation. + const int bW = rwidth * 20 / 100; + const int bH = rheight * 20 / 100; + calcMedians(baseImg, bW, bH, rwidth - bW, rheight - bH, rmed, gmed, bmed); + + if (settings->verbose) { + printf("FilmNeg legacy V2 :: Thumbnail input channel medians: %g %g %g\n", rmed, gmed, bmed); + } + + // Calculate output medians + rmed = powf(rmed, rexp); + gmed = powf(gmed, gexp); + bmed = powf(bmed, bexp); + + // Calculate output multipliers so that the median value is 1/24 of the output range. + rmult = (MAX_OUT_VALUE / 24.f) / rmed; + gmult = (MAX_OUT_VALUE / 24.f) / gmed; + bmult = (MAX_OUT_VALUE / 24.f) / bmed; + + } else { + + // Read film-base values from params + float rbase = params.filmNegative.refInput.r; // redBase; + float gbase = params.filmNegative.refInput.g; // greenBase; + float bbase = params.filmNegative.refInput.b; // blueBase; + + // Reconstruct scale_mul coefficients from thumbnail metadata: + // redMultiplier / camwbRed is pre_mul[0] + // pre_mul[0] * scaleGain is scale_mul[0] + // Apply channel scaling to raw (unscaled) input base values, to + // match with actual (scaled) data in baseImg + rbase *= (redMultiplier / camwbRed) * scaleGain; + gbase *= (greenMultiplier / camwbGreen) * scaleGain; + bbase *= (blueMultiplier / camwbBlue) * scaleGain; + + if (settings->verbose) { + printf("FilmNeg legacy V2 :: Thumbnail input film base values: %g %g %g\n", rbase, gbase, bbase); + } + + // Apply exponents to get output film base values + rbase = powf(rbase, rexp); + gbase = powf(gbase, gexp); + bbase = powf(bbase, bexp); + + // Calculate multipliers so that film base value is 1/512th of the output range. + rmult = (MAX_OUT_VALUE / 512.f) / rbase; + gmult = (MAX_OUT_VALUE / 512.f) / gbase; + bmult = (MAX_OUT_VALUE / 512.f) / bbase; + + } + + + // Get and un-apply multipliers to adapt the thumbnail to a known fixed WB setting, + // as in the main image processing. + + double r, g, b; + ColorTemp(3500., 1., 1., "Custom").getMultipliers(r, g, b); + //iColorMatrix is cam_rgb + const double rm = camwbRed / (iColorMatrix[0][0] * r + iColorMatrix[0][1] * g + iColorMatrix[0][2] * b); + const double gm = camwbGreen / (iColorMatrix[1][0] * r + iColorMatrix[1][1] * g + iColorMatrix[1][2] * b); + const double bm = camwbBlue / (iColorMatrix[2][0] * r + iColorMatrix[2][1] * g + iColorMatrix[2][2] * b); + + // Normalize max WB multiplier to 1.f + const double m = max(rm, gm, bm); + rmult /= rm / m; + gmult /= gm / m; + bmult /= bm / m; + + + if (settings->verbose) { + printf("FilmNeg legacy V2 :: Thumbnail computed multipliers: %g %g %g\n", static_cast(rmult), static_cast(gmult), static_cast(bmult)); + } + + +#ifdef __SSE2__ + const vfloat clipv = F2V(MAXVALF); + const vfloat rexpv = F2V(rexp); + const vfloat gexpv = F2V(gexp); + const vfloat bexpv = F2V(bexp); + const vfloat rmultv = F2V(rmult); + const vfloat gmultv = F2V(gmult); + const vfloat bmultv = F2V(bmult); +#endif + + for (int i = 0; i < rheight; i++) { + float *rline = baseImg->r(i); + float *gline = baseImg->g(i); + float *bline = baseImg->b(i); + int j = 0; +#ifdef __SSE2__ + + for (; j < rwidth - 3; j += 4) { + STVFU(rline[j], vminf(rmultv * pow_F(LVFU(rline[j]), rexpv), clipv)); + STVFU(gline[j], vminf(gmultv * pow_F(LVFU(gline[j]), gexpv), clipv)); + STVFU(bline[j], vminf(bmultv * pow_F(LVFU(bline[j]), bexpv), clipv)); + } + +#endif + + for (; j < rwidth; ++j) { + rline[j] = CLIP(rmult * pow_F(rline[j], rexp)); + gline[j] = CLIP(gmult * pow_F(gline[j], gexp)); + bline[j] = CLIP(bmult * pow_F(bline[j], bexp)); + } + } +} + +// ----------------- <<< legacy mode <<< ------------ + + diff --git a/rtengine/filmnegativethumb.cc b/rtengine/filmnegativethumb.cc deleted file mode 100644 index 57f2601f9..000000000 --- a/rtengine/filmnegativethumb.cc +++ /dev/null @@ -1,237 +0,0 @@ -/* - * This file is part of RawTherapee. - * - * Copyright (c) 2019 Alberto Romei - * - * RawTherapee is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * RawTherapee is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with RawTherapee. If not, see . - */ -#include - -#include "colortemp.h" -#include "LUT.h" -#include "rtengine.h" -#include "rtthumbnail.h" -#include "opthelper.h" -#include "sleef.h" -#include "rt_algo.h" -#include "settings.h" -#include "procparams.h" -#define BENCHMARK -#include "StopWatch.h" - -namespace -{ - -using rtengine::Imagefloat; -using rtengine::findMinMaxPercentile; - - -void calcMedians( - const Imagefloat* baseImg, - const int x1, const int y1, - const int x2, const int y2, - float &rmed, float &gmed, float &bmed -) -{ - // Channel vectors to calculate medians - std::vector rv, gv, bv; - - const int sz = std::max(0, (y2 - y1) * (x2 - x1)); - rv.reserve(sz); - gv.reserve(sz); - bv.reserve(sz); - - for (int i = y1; i < y2; i++) { - for (int j = x1; j < x2; j++) { - rv.push_back(baseImg->r(i, j)); - gv.push_back(baseImg->g(i, j)); - bv.push_back(baseImg->b(i, j)); - } - } - - // Calculate channel medians from whole image - findMinMaxPercentile(rv.data(), rv.size(), 0.5f, rmed, 0.5f, rmed, true); - findMinMaxPercentile(gv.data(), gv.size(), 0.5f, gmed, 0.5f, gmed, true); - findMinMaxPercentile(bv.data(), bv.size(), 0.5f, bmed, 0.5f, bmed, true); -} - -} - -void rtengine::Thumbnail::processFilmNegative( - const procparams::ProcParams ¶ms, - const Imagefloat* baseImg, - const int rwidth, const int rheight -) -{ - - // Channel exponents - const float rexp = -params.filmNegative.redRatio * params.filmNegative.greenExp; - const float gexp = -params.filmNegative.greenExp; - const float bexp = -params.filmNegative.blueRatio * params.filmNegative.greenExp; - - // Calculate output multipliers - float rmult, gmult, bmult; - - const float MAX_OUT_VALUE = 65000.f; - - // For backwards compatibility with profiles saved by RT 5.7 - const bool oldChannelScaling = params.filmNegative.redBase == -1.f; - - // If the film base values are not set in params, estimate multipliers from each channel's median value. - if (params.filmNegative.redBase <= 0.f) { - - // Channel medians - float rmed, gmed, bmed; - - if (oldChannelScaling) { - // If using the old method, calculate nedians on the whole image - calcMedians(baseImg, 0, 0, rwidth, rheight, rmed, gmed, bmed); - } else { - // The new method cuts out a 20% border from medians calculation. - const int bW = rwidth * 20 / 100; - const int bH = rheight * 20 / 100; - calcMedians(baseImg, bW, bH, rwidth - bW, rheight - bH, rmed, gmed, bmed); - } - - if (settings->verbose) { - printf("Thumbnail input channel medians: %g %g %g\n", rmed, gmed, bmed); - } - - // Calculate output medians - rmed = powf(rmed, rexp); - gmed = powf(gmed, gexp); - bmed = powf(bmed, bexp); - - // Calculate output multipliers so that the median value is 1/24 of the output range. - rmult = (MAX_OUT_VALUE / 24.f) / rmed; - gmult = (MAX_OUT_VALUE / 24.f) / gmed; - bmult = (MAX_OUT_VALUE / 24.f) / bmed; - - } else { - - // Read film-base values from params - float rbase = params.filmNegative.redBase; - float gbase = params.filmNegative.greenBase; - float bbase = params.filmNegative.blueBase; - - // Reconstruct scale_mul coefficients from thumbnail metadata: - // redMultiplier / camwbRed is pre_mul[0] - // pre_mul[0] * scaleGain is scale_mul[0] - // Apply channel scaling to raw (unscaled) input base values, to - // match with actual (scaled) data in baseImg - rbase *= (redMultiplier / camwbRed) * scaleGain; - gbase *= (greenMultiplier / camwbGreen) * scaleGain; - bbase *= (blueMultiplier / camwbBlue) * scaleGain; - - if (settings->verbose) { - printf("Thumbnail input film base values: %g %g %g\n", rbase, gbase, bbase); - } - - // Apply exponents to get output film base values - rbase = powf(rbase, rexp); - gbase = powf(gbase, gexp); - bbase = powf(bbase, bexp); - - if (settings->verbose) { - printf("Thumbnail output film base values: %g %g %g\n", rbase, gbase, bbase); - } - - // Calculate multipliers so that film base value is 1/512th of the output range. - rmult = (MAX_OUT_VALUE / 512.f) / rbase; - gmult = (MAX_OUT_VALUE / 512.f) / gbase; - bmult = (MAX_OUT_VALUE / 512.f) / bbase; - - } - - - if (oldChannelScaling) { - // Need to calculate channel averages, to fake the same conditions - // found in rawimagesource, where get_ColorsCoeff is called with - // forceAutoWB=true. - float rsum = 0.f, gsum = 0.f, bsum = 0.f; - - for (int i = 0; i < rheight; i++) { - for (int j = 0; j < rwidth; j++) { - rsum += baseImg->r(i, j); - gsum += baseImg->g(i, j); - bsum += baseImg->b(i, j); - } - } - - const float ravg = rsum / (rheight * rwidth); - const float gavg = gsum / (rheight * rwidth); - const float bavg = bsum / (rheight * rwidth); - - // Shifting current WB multipliers, based on channel averages. - rmult /= gavg / ravg; - // gmult /= gAvg / gAvg; green chosen as reference channel - bmult /= gavg / bavg; - - } else { - - // Get and un-apply multipliers to adapt the thumbnail to a known fixed WB setting, - // as in the main image processing. - - double r, g, b; - ColorTemp(3500., 1., 1., "Custom").getMultipliers(r, g, b); - //iColorMatrix is cam_rgb - const double rm = camwbRed / (iColorMatrix[0][0] * r + iColorMatrix[0][1] * g + iColorMatrix[0][2] * b); - const double gm = camwbGreen / (iColorMatrix[1][0] * r + iColorMatrix[1][1] * g + iColorMatrix[1][2] * b); - const double bm = camwbBlue / (iColorMatrix[2][0] * r + iColorMatrix[2][1] * g + iColorMatrix[2][2] * b); - - // Normalize max WB multiplier to 1.f - const double m = max(rm, gm, bm); - rmult /= rm / m; - gmult /= gm / m; - bmult /= bm / m; - } - - - if (settings->verbose) { - printf("Thumbnail computed multipliers: %g %g %g\n", static_cast(rmult), static_cast(gmult), static_cast(bmult)); - } - - -#ifdef __SSE2__ - const vfloat clipv = F2V(MAXVALF); - const vfloat rexpv = F2V(rexp); - const vfloat gexpv = F2V(gexp); - const vfloat bexpv = F2V(bexp); - const vfloat rmultv = F2V(rmult); - const vfloat gmultv = F2V(gmult); - const vfloat bmultv = F2V(bmult); -#endif - - for (int i = 0; i < rheight; i++) { - float *rline = baseImg->r(i); - float *gline = baseImg->g(i); - float *bline = baseImg->b(i); - int j = 0; -#ifdef __SSE2__ - - for (; j < rwidth - 3; j += 4) { - STVFU(rline[j], vminf(rmultv * pow_F(LVFU(rline[j]), rexpv), clipv)); - STVFU(gline[j], vminf(gmultv * pow_F(LVFU(gline[j]), gexpv), clipv)); - STVFU(bline[j], vminf(bmultv * pow_F(LVFU(bline[j]), bexpv), clipv)); - } - -#endif - - for (; j < rwidth; ++j) { - rline[j] = CLIP(rmult * pow_F(rline[j], rexp)); - gline[j] = CLIP(gmult * pow_F(gline[j], gexp)); - bline[j] = CLIP(bmult * pow_F(bline[j], bexp)); - } - } -} diff --git a/rtengine/imagesource.h b/rtengine/imagesource.h index ea049d37c..a1c3731f1 100644 --- a/rtengine/imagesource.h +++ b/rtengine/imagesource.h @@ -92,9 +92,6 @@ public: ~ImageSource () override {} virtual int load (const Glib::ustring &fname) = 0; virtual void preprocess (const procparams::RAWParams &raw, const procparams::LensProfParams &lensProf, const procparams::CoarseTransformParams& coarse, bool prepareDenoise = true) {}; - virtual void filmNegativeProcess (const procparams::FilmNegativeParams ¶ms, std::array& filmBaseValues) {}; - virtual bool getFilmNegativeExponents (Coord2D spotA, Coord2D spotB, int tran, const procparams::FilmNegativeParams& currentParams, std::array& newExps) { return false; }; - virtual bool getRawSpotValues (Coord2D spot, int spotSize, int tran, const procparams::FilmNegativeParams ¶ms, std::array& rawValues) { return false; }; virtual void demosaic (const procparams::RAWParams &raw, bool autoContrast, double &contrastThreshold, bool cache = false) {}; virtual void retinex (const procparams::ColorManagementParams& cmp, const procparams::RetinexParams &deh, const procparams::ToneCurveParams& Tc, LUTf & cdcurve, LUTf & mapcurve, const RetinextransmissionCurve & dehatransmissionCurve, const RetinexgaintransmissionCurve & dehagaintransmissionCurve, multi_array2D &conversionBuffer, bool dehacontlutili, bool mapcontlutili, bool useHsl, float &minCD, float &maxCD, float &mini, float &maxi, float &Tmean, float &Tsigma, float &Tmin, float &Tmax, LUTu &histLRETI) {}; virtual void retinexPrepareCurves (const procparams::RetinexParams &retinexParams, LUTf &cdcurve, LUTf &mapcurve, RetinextransmissionCurve &retinextransmissionCurve, RetinexgaintransmissionCurve &retinexgaintransmissionCurve, bool &retinexcontlutili, bool &mapcontlutili, bool &useHsl, LUTu & lhist16RETI, LUTu & histLRETI) {}; @@ -110,6 +107,7 @@ public: virtual int getFrameCount () = 0; virtual int getFlatFieldAutoClipValue () = 0; + virtual void getWBMults (const ColorTemp &ctemp, const procparams::RAWParams &raw, std::array& scale_mul, float &autoGainComp, float &rm, float &gm, float &bm) const = 0; // use right after demosaicing image, add coarse transformation and put the result in the provided Imagefloat* virtual void getImage (const ColorTemp &ctemp, int tran, Imagefloat* image, const PreviewProps &pp, const procparams::ToneCurveParams &hlp, const procparams::RAWParams &raw) = 0; diff --git a/rtengine/improccoordinator.cc b/rtengine/improccoordinator.cc index 09a226ded..e63bb19f4 100644 --- a/rtengine/improccoordinator.cc +++ b/rtengine/improccoordinator.cc @@ -48,19 +48,6 @@ namespace constexpr int VECTORSCOPE_SIZE = 128; -using rtengine::Coord2D; -Coord2D translateCoord(const rtengine::ImProcFunctions& ipf, int fw, int fh, int x, int y) { - - const std::vector points = {Coord2D(x, y)}; - - std::vector red; - std::vector green; - std::vector blue; - ipf.transCoord(fw, fh, points, red, green, blue); - - return green[0]; -} - } namespace rtengine @@ -369,26 +356,6 @@ void ImProcCoordinator::updatePreviewImage(int todo, bool panningRelatedChange) hist_raw_dirty = !(hListener && hListener->updateHistogramRaw()); highDetailPreprocessComputed = highDetailNeeded; - - // After preprocess, run film negative processing if enabled - if ( - (todo & M_RAW) - && ( - imgsrc->getSensorType() == ST_BAYER - || imgsrc->getSensorType() == ST_FUJI_XTRANS - ) - && params->filmNegative.enabled - ) { - std::array filmBaseValues = { - static_cast(params->filmNegative.redBase), - static_cast(params->filmNegative.greenBase), - static_cast(params->filmNegative.blueBase) - }; - imgsrc->filmNegativeProcess(params->filmNegative, filmBaseValues); - if (filmNegListener && params->filmNegative.redBase <= 0.f) { - filmNegListener->filmBaseValuesChanged(filmBaseValues); - } - } } /* @@ -660,7 +627,27 @@ void ImProcCoordinator::updatePreviewImage(int todo, bool panningRelatedChange) } } */ - imgsrc->convertColorSpace(orig_prev, params->icm, currWB); + + if (params->filmNegative.enabled) { + + // Process film negative AFTER colorspace conversion + if (params->filmNegative.colorSpace != FilmNegativeParams::ColorSpace::INPUT) { + imgsrc->convertColorSpace(orig_prev, params->icm, currWB); + } + + // Perform negative inversion. If needed, upgrade filmNegative params for backwards compatibility with old profiles + if (ipf.filmNegativeProcess(orig_prev, orig_prev, params->filmNegative, params->raw, imgsrc, currWB) && filmNegListener) { + filmNegListener->filmRefValuesChanged(params->filmNegative.refInput, params->filmNegative.refOutput); + } + + // Process film negative BEFORE colorspace conversion (legacy mode) + if (params->filmNegative.colorSpace == FilmNegativeParams::ColorSpace::INPUT) { + imgsrc->convertColorSpace(orig_prev, params->icm, currWB); + } + + } else { + imgsrc->convertColorSpace(orig_prev, params->icm, currWB); + } ipf.firstAnalysis(orig_prev, *params, vhist16); } @@ -2116,25 +2103,7 @@ void ImProcCoordinator::getSpotWB(int x, int y, int rect, double& temp, double& } } -bool ImProcCoordinator::getFilmNegativeExponents(int xA, int yA, int xB, int yB, std::array& newExps) -{ - MyMutex::MyLock lock(mProcessing); - const int tr = getCoarseBitMask(params->coarse); - - const Coord2D p1 = translateCoord(ipf, fw, fh, xA, yA); - const Coord2D p2 = translateCoord(ipf, fw, fh, xB, yB); - - return imgsrc->getFilmNegativeExponents(p1, p2, tr, params->filmNegative, newExps); -} - -bool ImProcCoordinator::getRawSpotValues(int x, int y, int spotSize, std::array& rawValues) -{ - MyMutex::MyLock lock(mProcessing); - - return imgsrc->getRawSpotValues(translateCoord(ipf, fw, fh, x, y), spotSize, - getCoarseBitMask(params->coarse), params->filmNegative, rawValues); -} void ImProcCoordinator::getAutoCrop(double ratio, int &x, int &y, int &w, int &h) { @@ -2405,6 +2374,7 @@ void ImProcCoordinator::process() || params->dirpyrequalizer != nextParams->dirpyrequalizer || params->dehaze != nextParams->dehaze || params->pdsharpening != nextParams->pdsharpening + || params->filmNegative != nextParams->filmNegative || sharpMaskChanged; sharpMaskChanged = false; diff --git a/rtengine/improccoordinator.h b/rtengine/improccoordinator.h index 3d84a0aad..8097dd9ce 100644 --- a/rtengine/improccoordinator.h +++ b/rtengine/improccoordinator.h @@ -407,8 +407,7 @@ public: bool getAutoWB (double& temp, double& green, double equal, double tempBias) override; void getCamWB (double& temp, double& green) override; void getSpotWB (int x, int y, int rectSize, double& temp, double& green) override; - bool getFilmNegativeExponents(int xA, int yA, int xB, int yB, std::array& newExps) override; - bool getRawSpotValues(int x, int y, int spotSize, std::array& rawValues) override; + bool getFilmNegativeSpot(int x, int y, int spotSize, FilmNegativeParams::RGB &refInput, FilmNegativeParams::RGB &refOutput) override; void getAutoCrop (double ratio, int &x, int &y, int &w, int &h) override; bool getHighQualComputed() override; void setHighQualComputed() override; diff --git a/rtengine/improcfun.h b/rtengine/improcfun.h index aa24aac6a..e7b6b176d 100644 --- a/rtengine/improcfun.h +++ b/rtengine/improcfun.h @@ -88,6 +88,7 @@ struct DehazeParams; struct FattalToneMappingParams; struct ColorManagementParams; struct DirPyrDenoiseParams; +struct FilmNegativeParams; struct LocalContrastParams; struct LocallabParams; struct SharpeningParams; @@ -149,6 +150,12 @@ public: bool needsTransform(int oW, int oH, int rawRotationDeg, const FramesMetaData *metadata) const; bool needsPCVignetting() const; + + bool filmNegativeProcess(rtengine::Imagefloat *input, rtengine::Imagefloat *output, procparams::FilmNegativeParams &fnp, + const procparams::RAWParams &rawParams, const rtengine::ImageSource* imgsrc, const rtengine::ColorTemp &currWB); + + void filmNegativeProcess(rtengine::Imagefloat *input, rtengine::Imagefloat *output, const procparams::FilmNegativeParams ¶ms); + float calcGradientFactor (const struct grad_params& gp, int x, int y); void firstAnalysis(const Imagefloat* const working, const procparams::ProcParams ¶ms, LUTu & vhist16); void updateColorProfiles(const Glib::ustring& monitorProfile, RenderingIntent monitorIntent, bool softProof, bool gamutCheck); diff --git a/rtengine/procparams.cc b/rtengine/procparams.cc index fef32b05d..e6f06480d 100644 --- a/rtengine/procparams.cc +++ b/rtengine/procparams.cc @@ -112,6 +112,16 @@ void getFromKeyfile( value = keyfile.get_double(group_name, key); } +void getFromKeyfile( + const Glib::KeyFile& keyfile, + const Glib::ustring& group_name, + const Glib::ustring& key, + float& value +) +{ + value = static_cast(keyfile.get_double(group_name, key)); +} + void getFromKeyfile( const Glib::KeyFile& keyfile, const Glib::ustring& group_name, @@ -153,6 +163,21 @@ void getFromKeyfile( rtengine::sanitizeCurve(value); } +void getFromKeyfile( + const Glib::KeyFile& keyfile, + const Glib::ustring& group_name, + const Glib::ustring& key, + rtengine::procparams::FilmNegativeParams::RGB& value +) +{ + const std::vector v = keyfile.get_double_list(group_name, key); + if(v.size() >= 3) { + value.r = v[0]; + value.g = v[1]; + value.b = v[2]; + } +} + template bool assignFromKeyfile( const Glib::KeyFile& keyfile, @@ -219,6 +244,16 @@ void putToKeyfile( keyfile.set_integer(group_name, key, value); } +void putToKeyfile( + const Glib::ustring& group_name, + const Glib::ustring& key, + float value, + Glib::KeyFile& keyfile +) +{ + keyfile.set_double(group_name, key, static_cast(value)); +} + void putToKeyfile( const Glib::ustring& group_name, const Glib::ustring& key, @@ -271,6 +306,18 @@ void putToKeyfile( keyfile.set_double_list(group_name, key, list); } +void putToKeyfile( + const Glib::ustring& group_name, + const Glib::ustring& key, + const rtengine::procparams::FilmNegativeParams::RGB& value, + Glib::KeyFile& keyfile +) +{ + const std::vector vec = { value.r, value.g, value.b }; + keyfile.set_double_list(group_name, key, vec); +} + + template bool saveToKeyfile( bool save, @@ -5013,12 +5060,35 @@ FilmNegativeParams::FilmNegativeParams() : redRatio(1.36), greenExp(1.5), blueRatio(0.86), - redBase(0), - greenBase(0), - blueBase(0) + refInput({0.0, 0.0, 0.0}), + refOutput({0.0, 0.0, 0.0}), + colorSpace(ColorSpace::WORKING), + backCompat(BackCompat::CURRENT) { } +bool FilmNegativeParams::RGB::operator ==(const FilmNegativeParams::RGB& other) const +{ + return + r == other.r + && g == other.g + && b == other.b; +} + +bool FilmNegativeParams::RGB::operator !=(const FilmNegativeParams::RGB& other) const +{ + return !(*this == other); +} + +FilmNegativeParams::RGB FilmNegativeParams::RGB::operator *(const FilmNegativeParams::RGB& other) const +{ + return { + (*this).r * other.r, + (*this).g * other.g, + (*this).b * other.b + }; +} + bool FilmNegativeParams::operator ==(const FilmNegativeParams& other) const { return @@ -5026,9 +5096,10 @@ bool FilmNegativeParams::operator ==(const FilmNegativeParams& other) const && redRatio == other.redRatio && greenExp == other.greenExp && blueRatio == other.blueRatio - && redBase == other.redBase - && greenBase == other.greenBase - && blueBase == other.blueBase; + && refInput == other.refInput + && refOutput == other.refOutput + && colorSpace == other.colorSpace + && backCompat == other.backCompat; } bool FilmNegativeParams::operator !=(const FilmNegativeParams& other) const @@ -6506,9 +6577,18 @@ int ProcParams::save(const Glib::ustring& fname, const Glib::ustring& fname2, bo saveToKeyfile(!pedited || pedited->filmNegative.redRatio, "Film Negative", "RedRatio", filmNegative.redRatio, keyFile); saveToKeyfile(!pedited || pedited->filmNegative.greenExp, "Film Negative", "GreenExponent", filmNegative.greenExp, keyFile); saveToKeyfile(!pedited || pedited->filmNegative.blueRatio, "Film Negative", "BlueRatio", filmNegative.blueRatio, keyFile); - saveToKeyfile(!pedited || pedited->filmNegative.baseValues, "Film Negative", "RedBase", filmNegative.redBase, keyFile); - saveToKeyfile(!pedited || pedited->filmNegative.baseValues, "Film Negative", "GreenBase", filmNegative.greenBase, keyFile); - saveToKeyfile(!pedited || pedited->filmNegative.baseValues, "Film Negative", "BlueBase", filmNegative.blueBase, keyFile); + + // FIXME to be removed: only for backwards compatibility with an intermediate dev version + if (filmNegative.backCompat == FilmNegativeParams::BackCompat::V2) { + saveToKeyfile(!pedited || pedited->filmNegative.refInput, "Film Negative", "RedBase", filmNegative.refInput.r, keyFile); + saveToKeyfile(!pedited || pedited->filmNegative.refInput, "Film Negative", "GreenBase", filmNegative.refInput.g, keyFile); + saveToKeyfile(!pedited || pedited->filmNegative.refInput, "Film Negative", "BlueBase", filmNegative.refInput.b, keyFile); + } + + saveToKeyfile(!pedited || pedited->filmNegative.colorSpace, "Film Negative", "ColorSpace", toUnderlying(filmNegative.colorSpace), keyFile); + saveToKeyfile(!pedited || pedited->filmNegative.refInput, "Film Negative", "RefInput", filmNegative.refInput, keyFile); + saveToKeyfile(!pedited || pedited->filmNegative.refOutput, "Film Negative", "RefOutput", filmNegative.refOutput, keyFile); + saveToKeyfile(true, "Film Negative", "BackCompat", toUnderlying(filmNegative.backCompat), keyFile); // Preprocess WB saveToKeyfile(!pedited || pedited->raw.preprocessWB.mode, "RAW Preprocess WB", "Mode", toUnderlying(raw.preprocessWB.mode), keyFile); @@ -8868,23 +8948,48 @@ int ProcParams::load(const Glib::ustring& fname, ParamsEdited* pedited) assignFromKeyfile(keyFile, "Film Negative", "RedRatio", pedited, filmNegative.redRatio, pedited->filmNegative.redRatio); assignFromKeyfile(keyFile, "Film Negative", "GreenExponent", pedited, filmNegative.greenExp, pedited->filmNegative.greenExp); assignFromKeyfile(keyFile, "Film Negative", "BlueRatio", pedited, filmNegative.blueRatio, pedited->filmNegative.blueRatio); - if (ppVersion >= 347) { + + if (ppVersion < 347) { + // Backwards compatibility with RT v5.8 + filmNegative.colorSpace = FilmNegativeParams::ColorSpace::INPUT; + filmNegative.backCompat = FilmNegativeParams::BackCompat::V1; + if (pedited) { + pedited->filmNegative.refInput = true; + pedited->filmNegative.refOutput = true; + pedited->filmNegative.colorSpace = true; + } + + } else if (!keyFile.has_key("Film Negative", "RefInput")) { + // Backwards compatibility with intermediate dev version (after v5.8) using film base values bool r, g, b; - assignFromKeyfile(keyFile, "Film Negative", "RedBase", pedited, filmNegative.redBase, r); - assignFromKeyfile(keyFile, "Film Negative", "GreenBase", pedited, filmNegative.greenBase, g); - assignFromKeyfile(keyFile, "Film Negative", "BlueBase", pedited, filmNegative.blueBase, b); + assignFromKeyfile(keyFile, "Film Negative", "RedBase", pedited, filmNegative.refInput.r, r); + assignFromKeyfile(keyFile, "Film Negative", "GreenBase", pedited, filmNegative.refInput.g, g); + assignFromKeyfile(keyFile, "Film Negative", "BlueBase", pedited, filmNegative.refInput.b, b); if (pedited) { - pedited->filmNegative.baseValues = r || g || b; + pedited->filmNegative.refInput = r || g || b; + pedited->filmNegative.refOutput = r || g || b; + pedited->filmNegative.colorSpace = true; } - } else { - // Backwards compatibility with film negative in RT 5.7: use special film base value -1, - // to signal that the old channel scaling method should be used. - filmNegative.redBase = -1.f; - filmNegative.greenBase = -1.f; - filmNegative.blueBase = -1.f; - if (pedited) { - pedited->filmNegative.baseValues = true; + + filmNegative.colorSpace = FilmNegativeParams::ColorSpace::INPUT; + // Special value -1 used to mean that this should be treated as a v5.8 profile + filmNegative.backCompat = (filmNegative.refInput.r == -1.f) + ? FilmNegativeParams::BackCompat::V1 + : FilmNegativeParams::BackCompat::V2; + + } else { // current version + + assignFromKeyfile(keyFile, "Film Negative", "RefInput", pedited, filmNegative.refInput, pedited->filmNegative.refInput); + assignFromKeyfile(keyFile, "Film Negative", "RefOutput", pedited, filmNegative.refOutput, pedited->filmNegative.refOutput); + + int cs = toUnderlying(filmNegative.colorSpace); + assignFromKeyfile(keyFile, "Film Negative", "ColorSpace", pedited, cs, pedited->filmNegative.colorSpace); + filmNegative.colorSpace = static_cast(cs); + + if (keyFile.has_key("Film Negative", "BackCompat")) { + filmNegative.backCompat = FilmNegativeParams::BackCompat(keyFile.get_integer("Film Negative", "BackCompat")); } + } } diff --git a/rtengine/procparams.h b/rtengine/procparams.h index 39992d88b..0a3377472 100644 --- a/rtengine/procparams.h +++ b/rtengine/procparams.h @@ -2184,10 +2184,28 @@ struct FilmNegativeParams { double greenExp; double blueRatio; - double redBase; - double greenBase; - double blueBase; - + struct RGB { + float r, g, b; + + bool operator ==(const RGB& other) const; + bool operator !=(const RGB& other) const; + RGB operator *(const RGB& other) const; + }; + + RGB refInput; + RGB refOutput; + + enum class ColorSpace { + INPUT = 0, + WORKING + // TODO : add support for custom color profile + }; + + ColorSpace colorSpace; + + enum class BackCompat { CURRENT = 0, V1, V2 }; + BackCompat backCompat; + FilmNegativeParams(); bool operator ==(const FilmNegativeParams& other) const; diff --git a/rtengine/rawimagesource.cc b/rtengine/rawimagesource.cc index 8bfcaa5ab..689dddf20 100644 --- a/rtengine/rawimagesource.cc +++ b/rtengine/rawimagesource.cc @@ -636,6 +636,56 @@ float calculate_scale_mul(float scale_mul[4], const float pre_mul_[4], const flo return gain; } + +void RawImageSource::getWBMults (const ColorTemp &ctemp, const RAWParams &raw, std::array& out_scale_mul, float &autoGainComp, float &rm, float &gm, float &bm) const +{ + // compute channel multipliers + double r, g, b; + //float rm, gm, bm; + + if (ctemp.getTemp() < 0) { + // no white balance, ie revert the pre-process white balance to restore original unbalanced raw camera color + rm = ri->get_pre_mul(0); + gm = ri->get_pre_mul(1); + bm = ri->get_pre_mul(2); + } else { + ctemp.getMultipliers (r, g, b); + rm = imatrices.cam_rgb[0][0] * r + imatrices.cam_rgb[0][1] * g + imatrices.cam_rgb[0][2] * b; + gm = imatrices.cam_rgb[1][0] * r + imatrices.cam_rgb[1][1] * g + imatrices.cam_rgb[1][2] * b; + bm = imatrices.cam_rgb[2][0] * r + imatrices.cam_rgb[2][1] * g + imatrices.cam_rgb[2][2] * b; + } + + // adjust gain so the maximum raw value of the least scaled channel just hits max + const float new_pre_mul[4] = { ri->get_pre_mul(0) / rm, ri->get_pre_mul(1) / gm, ri->get_pre_mul(2) / bm, ri->get_pre_mul(3) / gm }; + float new_scale_mul[4]; + + bool isMono = (ri->getSensorType() == ST_FUJI_XTRANS && raw.xtranssensor.method == RAWParams::XTransSensor::getMethodString(RAWParams::XTransSensor::Method::MONO)) + || (ri->getSensorType() == ST_BAYER && raw.bayersensor.method == RAWParams::BayerSensor::getMethodString(RAWParams::BayerSensor::Method::MONO)); + + float c_white[4]; + for (int i = 0; i < 4; ++i) { + c_white[i] = (ri->get_white(i) - cblacksom[i]) / static_cast(raw.expos) + cblacksom[i]; + } + + float gain = calculate_scale_mul(new_scale_mul, new_pre_mul, c_white, cblacksom, isMono, ri->get_colors()); + rm = new_scale_mul[0] / scale_mul[0] * gain; + gm = new_scale_mul[1] / scale_mul[1] * gain; + bm = new_scale_mul[2] / scale_mul[2] * gain; + //fprintf(stderr, "camera gain: %f, current wb gain: %f, diff in stops %f\n", camInitialGain, gain, log2(camInitialGain) - log2(gain)); + + const float expcomp = std::pow(2, ri->getBaselineExposure()); + rm *= expcomp; + gm *= expcomp; + bm *= expcomp; + + out_scale_mul[0] = scale_mul[0]; + out_scale_mul[1] = scale_mul[1]; + out_scale_mul[2] = scale_mul[2]; + out_scale_mul[3] = scale_mul[3]; + + autoGainComp = camInitialGain / initialGain; +} + void RawImageSource::getImage (const ColorTemp &ctemp, int tran, Imagefloat* image, const PreviewProps &pp, const ToneCurveParams &hrp, const RAWParams &raw) { MyMutex::MyLock lock(getImageMutex); diff --git a/rtengine/rawimagesource.h b/rtengine/rawimagesource.h index 12db6be74..b463ea788 100644 --- a/rtengine/rawimagesource.h +++ b/rtengine/rawimagesource.h @@ -124,9 +124,6 @@ public: int load(const Glib::ustring &fname) override { return load(fname, false); } int load(const Glib::ustring &fname, bool firstFrameOnly); void preprocess (const procparams::RAWParams &raw, const procparams::LensProfParams &lensProf, const procparams::CoarseTransformParams& coarse, bool prepareDenoise = true) override; - void filmNegativeProcess (const procparams::FilmNegativeParams ¶ms, std::array& filmBaseValues) override; - bool getFilmNegativeExponents (Coord2D spotA, Coord2D spotB, int tran, const procparams::FilmNegativeParams ¤tParams, std::array& newExps) override; - bool getRawSpotValues(Coord2D spot, int spotSize, int tran, const procparams::FilmNegativeParams ¶ms, std::array& rawValues) override; void demosaic (const procparams::RAWParams &raw, bool autoContrast, double &contrastThreshold, bool cache = false) override; void retinex (const procparams::ColorManagementParams& cmp, const procparams::RetinexParams &deh, const procparams::ToneCurveParams& Tc, LUTf & cdcurve, LUTf & mapcurve, const RetinextransmissionCurve & dehatransmissionCurve, const RetinexgaintransmissionCurve & dehagaintransmissionCurve, multi_array2D &conversionBuffer, bool dehacontlutili, bool mapcontlutili, bool useHsl, float &minCD, float &maxCD, float &mini, float &maxi, float &Tmean, float &Tsigma, float &Tmin, float &Tmax, LUTu &histLRETI) override; void retinexPrepareCurves (const procparams::RetinexParams &retinexParams, LUTf &cdcurve, LUTf &mapcurve, RetinextransmissionCurve &retinextransmissionCurve, RetinexgaintransmissionCurve &retinexgaintransmissionCurve, bool &retinexcontlutili, bool &mapcontlutili, bool &useHsl, LUTu & lhist16RETI, LUTu & histLRETI) override; @@ -147,6 +144,7 @@ public: void getAutoWBMultipliersitc(double &tempref, double &greenref, double &tempitc, double &greenitc, float &studgood, int begx, int begy, int yEn, int xEn, int cx, int cy, int bf_h, int bf_w, double &rm, double &gm, double &bm, const procparams::WBParams & wbpar, const procparams::ColorManagementParams &cmp, const procparams::RAWParams &raw) override; void getrgbloc(int begx, int begy, int yEn, int xEn, int cx, int cy, int bf_h, int bf_w) override; + void getWBMults (const ColorTemp &ctemp, const procparams::RAWParams &raw, std::array& scale_mul, float &autoGainComp, float &rm, float &gm, float &bm) const override; void getImage (const ColorTemp &ctemp, int tran, Imagefloat* image, const PreviewProps &pp, const procparams::ToneCurveParams &hrp, const procparams::RAWParams &raw) override; eSensorType getSensorType () const override; bool isMono () const override; diff --git a/rtengine/rtengine.h b/rtengine/rtengine.h index f9832ab9a..c4ffb91f7 100644 --- a/rtengine/rtengine.h +++ b/rtengine/rtengine.h @@ -510,7 +510,7 @@ class FilmNegListener { public: virtual ~FilmNegListener() = default; - virtual void filmBaseValuesChanged(std::array rgb) = 0; + virtual void filmRefValuesChanged(const procparams::FilmNegativeParams::RGB &refInput, const procparams::FilmNegativeParams::RGB &refOutput) = 0; }; /** This class represents a detailed part of the image (looking through a kind of window). @@ -599,9 +599,8 @@ public: virtual bool getAutoWB (double& temp, double& green, double equal, double tempBias) = 0; virtual void getCamWB (double& temp, double& green) = 0; virtual void getSpotWB (int x, int y, int rectSize, double& temp, double& green) = 0; - virtual bool getFilmNegativeExponents(int xA, int yA, int xB, int yB, std::array& newExps) = 0; - virtual bool getRawSpotValues (int x, int y, int spotSize, std::array& rawValues) = 0; - + virtual bool getFilmNegativeSpot(int x, int y, int spotSize, procparams::FilmNegativeParams::RGB &refInput, procparams::FilmNegativeParams::RGB &refOutput) = 0; + virtual void getAutoCrop (double ratio, int &x, int &y, int &w, int &h) = 0; virtual void saveInputICCReference (const Glib::ustring& fname, bool apply_wb) = 0; diff --git a/rtengine/rtthumbnail.cc b/rtengine/rtthumbnail.cc index 7d0f40f70..b724992ff 100644 --- a/rtengine/rtthumbnail.cc +++ b/rtengine/rtthumbnail.cc @@ -603,7 +603,7 @@ Thumbnail* Thumbnail::loadFromRaw (const Glib::ustring& fname, RawMetaDataLocati tpp->defGain = max (scale_mul[0], scale_mul[1], scale_mul[2], scale_mul[3]) / min (scale_mul[0], scale_mul[1], scale_mul[2], scale_mul[3]); tpp->defGain *= std::pow(2, ri->getBaselineExposure()); - tpp->scaleGain = scale_mul[0] / pre_mul[0]; // used to reconstruct scale_mul from filmnegativethumb.cc + tpp->scaleGain = scale_mul[0] / pre_mul[0]; // can be used to reconstruct scale_mul later in processing tpp->gammaCorrected = true; @@ -1192,8 +1192,13 @@ IImage8* Thumbnail::processImage (const procparams::ProcParams& params, eSensorT Imagefloat* baseImg = resizeTo (rwidth, rheight, interp, thumbImg); - if (isRaw && params.filmNegative.enabled) { - processFilmNegative(params, baseImg, rwidth, rheight); + // Film negative legacy mode, for backwards compatibility RT v5.8 + if (params.filmNegative.enabled) { + if (params.filmNegative.backCompat == FilmNegativeParams::BackCompat::V1) { + processFilmNegative(params, baseImg, rwidth, rheight); + } else if (params.filmNegative.backCompat == FilmNegativeParams::BackCompat::V2) { + processFilmNegativeV2(params, baseImg, rwidth, rheight); + } } if (params.coarse.rotate) { @@ -1235,6 +1240,19 @@ IImage8* Thumbnail::processImage (const procparams::ProcParams& params, eSensorT // if luma denoise has to be done for thumbnails, it should be right here + int fw = baseImg->getWidth(); + int fh = baseImg->getHeight(); + //ColorTemp::CAT02 (baseImg, ¶ms) ;//perhaps not good! + + ImProcFunctions ipf (¶ms, forHistogramMatching); // enable multithreading when forHistogramMatching is true + ipf.setScale (sqrt (double (fw * fw + fh * fh)) / sqrt (double (thumbImg->getWidth() * thumbImg->getWidth() + thumbImg->getHeight() * thumbImg->getHeight()))*scale); + ipf.updateColorProfiles (ICCStore::getInstance()->getDefaultMonitorProfileName(), RenderingIntent(settings->monitorIntent), false, false); + + // Process film negative BEFORE colorspace conversion, if needed + if (params.filmNegative.enabled && params.filmNegative.backCompat == FilmNegativeParams::BackCompat::CURRENT && params.filmNegative.colorSpace == FilmNegativeParams::ColorSpace::INPUT) { + ipf.filmNegativeProcess(baseImg, baseImg, params.filmNegative); + } + // perform color space transformation if (isRaw) { @@ -1244,13 +1262,10 @@ IImage8* Thumbnail::processImage (const procparams::ProcParams& params, eSensorT StdImageSource::colorSpaceConversion (baseImg, params.icm, embProfile, thumbImg->getSampleFormat()); } - int fw = baseImg->getWidth(); - int fh = baseImg->getHeight(); - //ColorTemp::CAT02 (baseImg, ¶ms) ;//perhaps not good! - - ImProcFunctions ipf (¶ms, forHistogramMatching); // enable multithreading when forHistogramMatching is true - ipf.setScale (sqrt (double (fw * fw + fh * fh)) / sqrt (double (thumbImg->getWidth() * thumbImg->getWidth() + thumbImg->getHeight() * thumbImg->getHeight()))*scale); - ipf.updateColorProfiles (ICCStore::getInstance()->getDefaultMonitorProfileName(), RenderingIntent(settings->monitorIntent), false, false); + // Process film negative AFTER colorspace conversion, if needed + if (params.filmNegative.enabled && params.filmNegative.backCompat == FilmNegativeParams::BackCompat::CURRENT && params.filmNegative.colorSpace != FilmNegativeParams::ColorSpace::INPUT) { + ipf.filmNegativeProcess(baseImg, baseImg, params.filmNegative); + } LUTu hist16 (65536); diff --git a/rtengine/rtthumbnail.h b/rtengine/rtthumbnail.h index a0033d35f..6ec1dfb34 100644 --- a/rtengine/rtthumbnail.h +++ b/rtengine/rtthumbnail.h @@ -78,6 +78,7 @@ class Thumbnail double scaleGain; void processFilmNegative(const procparams::ProcParams& params, const Imagefloat* baseImg, int rwidth, int rheight); + void processFilmNegativeV2(const procparams::ProcParams& params, const Imagefloat* baseImg, int rwidth, int rheight); public: diff --git a/rtengine/simpleprocess.cc b/rtengine/simpleprocess.cc index 6f0c3b29a..57e239b66 100644 --- a/rtengine/simpleprocess.cc +++ b/rtengine/simpleprocess.cc @@ -219,16 +219,6 @@ private: imgsrc->setCurrentFrame(params.raw.bayersensor.imageNum); imgsrc->preprocess(params.raw, params.lensProf, params.coarse, params.dirpyrDenoise.enabled); - // After preprocess, run film negative processing if enabled - if ((imgsrc->getSensorType() == ST_BAYER || (imgsrc->getSensorType() == ST_FUJI_XTRANS)) && params.filmNegative.enabled) { - std::array filmBaseValues = { - static_cast(params.filmNegative.redBase), - static_cast(params.filmNegative.greenBase), - static_cast(params.filmNegative.blueBase) - }; - imgsrc->filmNegativeProcess (params.filmNegative, filmBaseValues); - } - if (pl) { pl->setProgress(0.20); } @@ -873,7 +863,23 @@ private: //ImProcFunctions ipf (¶ms, true); ImProcFunctions &ipf = * (ipf_p.get()); - imgsrc->convertColorSpace(baseImg, params.icm, currWB); + if (params.filmNegative.enabled) { + // Process film negative AFTER colorspace conversion if camera space is NOT selected + if (params.filmNegative.colorSpace != FilmNegativeParams::ColorSpace::INPUT) { + imgsrc->convertColorSpace(baseImg, params.icm, currWB); + } + + FilmNegativeParams copy = params.filmNegative; + ipf.filmNegativeProcess(baseImg, baseImg, copy, params.raw, imgsrc, currWB); + + // ... otherwise, process film negative BEFORE colorspace conversion + if (params.filmNegative.colorSpace == FilmNegativeParams::ColorSpace::INPUT) { + imgsrc->convertColorSpace(baseImg, params.icm, currWB); + } + + } else { + imgsrc->convertColorSpace(baseImg, params.icm, currWB); + } // perform first analysis hist16(65536); diff --git a/rtengine/stdimagesource.h b/rtengine/stdimagesource.h index f937188b4..9b95fe34e 100644 --- a/rtengine/stdimagesource.h +++ b/rtengine/stdimagesource.h @@ -57,6 +57,7 @@ public: ~StdImageSource () override; int load (const Glib::ustring &fname) override; + void getWBMults (const ColorTemp &ctemp, const procparams::RAWParams &raw, std::array& scale_mul, float &autoGainComp, float &rm, float &gm, float &bm) const override {}; void getImage (const ColorTemp &ctemp, int tran, Imagefloat* image, const PreviewProps &pp, const procparams::ToneCurveParams &hrp, const procparams::RAWParams &raw) override; void getrgbloc (int begx, int begy, int yEn, int xEn, int cx, int cy, int bf_h, int bf_w) override {}; ColorTemp getWB () const override diff --git a/rtgui/filmnegative.cc b/rtgui/filmnegative.cc index d70c2a067..4df9e9a09 100644 --- a/rtgui/filmnegative.cc +++ b/rtgui/filmnegative.cc @@ -26,13 +26,24 @@ #include "rtimage.h" #include "../rtengine/procparams.h" +#include "../rtengine/color.h" namespace { -Adjuster* createExponentAdjuster(AdjusterListener* listener, const Glib::ustring& label, double minV, double maxV, double defaultVal) +double toAdjuster(double v) { - Adjuster* const adj = Gtk::manage(new Adjuster(label, minV, maxV, 0.001, defaultVal)); + return CLAMP(std::log2(v), 6, 16) - 6; +} + +double fromAdjuster(double v) +{ + return std::pow(2, v + 6); +} + +Adjuster* createExponentAdjuster(AdjusterListener* listener, const Glib::ustring& label, double minV, double maxV, double step, double defaultVal) +{ + Adjuster* const adj = Gtk::manage(new Adjuster(label, minV, maxV, step, defaultVal)); adj->setAdjusterListener(listener); adj->setLogScale(6, 1, true); @@ -42,46 +53,169 @@ Adjuster* createExponentAdjuster(AdjusterListener* listener, const Glib::ustring return adj; } -Glib::ustring formatBaseValues(const std::array& rgb) +Adjuster* createLevelAdjuster(AdjusterListener* listener, const Glib::ustring& label) { - if (rgb[0] <= 0.f && rgb[1] <= 0.f && rgb[2] <= 0.f) { +// Adjuster* const adj = Gtk::manage(new Adjuster(label, 1.0, 65535.0, 1.0, rtengine::MAXVALF / 24.)); + Adjuster* const adj = Gtk::manage(new Adjuster(label, 0.0, 10.0, 0.01, toAdjuster(rtengine::MAXVALF / 24.))); + adj->setAdjusterListener(listener); +// adj->setLogScale(6, 1000.0, true); + + adj->setDelay(std::max(options.adjusterMinDelay, options.adjusterMaxDelay)); + + adj->show(); + return adj; +} + +Adjuster* createBalanceAdjuster(AdjusterListener* listener, const Glib::ustring& label, double minV, double maxV, double defaultVal, + const Glib::ustring& leftIcon, const Glib::ustring& rightIcon) +{ + Adjuster* const adj = Gtk::manage(new Adjuster(label, minV, maxV, 0.01, defaultVal, + Gtk::manage(new RTImage(leftIcon)), Gtk::manage(new RTImage(rightIcon)))); + adj->setAdjusterListener(listener); + adj->setLogScale(9, 0, true); + + adj->setDelay(std::max(options.adjusterMinDelay, options.adjusterMaxDelay)); + + adj->show(); + return adj; +} + + +Glib::ustring fmt(const RGB& rgb) +{ + if (rgb.r <= 0.f && rgb.g <= 0.f && rgb.b <= 0.f) { return "- - -"; } else { - return Glib::ustring::format(std::fixed, std::setprecision(1), rgb[0]) + " " + - Glib::ustring::format(std::fixed, std::setprecision(1), rgb[1]) + " " + - Glib::ustring::format(std::fixed, std::setprecision(1), rgb[2]); + return Glib::ustring::format(std::fixed, std::setprecision(1), rgb.r) + " " + + Glib::ustring::format(std::fixed, std::setprecision(1), rgb.g) + " " + + Glib::ustring::format(std::fixed, std::setprecision(1), rgb.b); } } + +RGB getFilmNegativeExponents(const RGB &ref1, const RGB &ref2) // , const RGB &clearValsOut, const RGB &denseValsOut) +{ + using rtengine::settings; + + RGB clearVals = ref1; + RGB denseVals = ref2; + + // Detect which one is the dense spot, based on green channel + if (clearVals.g < denseVals.g) { + std::swap(clearVals, denseVals); + //std::swap(clearValsOut, denseValsOut); + } + + if (settings->verbose) { + printf("Clear input values: R=%g G=%g B=%g\n", static_cast(clearVals.r), static_cast(clearVals.g), static_cast(clearVals.b)); + printf("Dense input values: R=%g G=%g B=%g\n", static_cast(denseVals.r), static_cast(denseVals.g), static_cast(denseVals.b)); + + // printf("Clear output values: R=%g G=%g B=%g\n", static_cast(clearValsOut.r), static_cast(clearValsOut.g), static_cast(clearValsOut.b)); + // printf("Dense output values: R=%g G=%g B=%g\n", static_cast(denseValsOut.r), static_cast(denseValsOut.g), static_cast(denseValsOut.b)); + } + + const float denseGreenRatio = clearVals.g / denseVals.g; + + // Calculate logarithms in arbitrary base + const auto logBase = + [](float base, float num) -> float + { + return std::log(num) / std::log(base); + }; + + // const auto ratio = + // [](float a, float b) -> float + // { + // return a > b ? a / b : b / a; + // }; + + RGB newExps; + newExps.r = logBase(clearVals.r / denseVals.r, denseGreenRatio); + newExps.g = 1.f; // logBase(ratio(clearVals.g, denseVals.g), ratio(denseValsOut.g, clearValsOut.g) ); + newExps.b = logBase(clearVals.b / denseVals.b, denseGreenRatio); + + + + if (settings->verbose) { + printf("New exponents: R=%g G=%g B=%g\n", static_cast(newExps.r), static_cast(newExps.g), static_cast(newExps.b)); + } + + // // Re-adjust color balance based on dense spot values and new exponents + // calcBalance(rtengine::max(static_cast(params->filmNegative.refInput.g), 1.f), + // -newExps[0], -newExps[1], -newExps[2], + // denseVals[0], denseVals[1], denseVals[2], + // rBal, bBal); + + return newExps; + +} + +void temp2rgb(double outLev, double temp, double green, RGB &refOut) +{ + rtengine::ColorTemp ct = rtengine::ColorTemp(temp, green, 1., "Custom"); + + double rm, gm, bm; + ct.getMultipliers(rm, gm, bm); + + double maxGain = rtengine::max(rm, gm, bm); + + refOut.r = (rm / maxGain) * outLev; + refOut.g = (gm / maxGain) * outLev; + refOut.b = (bm / maxGain) * outLev; +} + + +void rgb2temp(const RGB &refOut, double &outLev, double &temp, double &green) +{ + double maxVal = rtengine::max(refOut.r, refOut.g, refOut.b); + + rtengine::ColorTemp ct = rtengine::ColorTemp( + refOut.r / maxVal, + refOut.g / maxVal, + refOut.b / maxVal, + 1.); + + outLev = maxVal; + temp = ct.getTemp(); + green = ct.getGreen(); +} + + } FilmNegative::FilmNegative() : FoldableToolPanel(this, "filmnegative", M("TP_FILMNEGATIVE_LABEL"), false, true), EditSubscriber(ET_OBJECTS), - evFilmNegativeExponents(ProcEventMapper::getInstance()->newEvent(FIRST, "HISTORY_MSG_FILMNEGATIVE_VALUES")), - evFilmNegativeEnabled(ProcEventMapper::getInstance()->newEvent(FIRST, "HISTORY_MSG_FILMNEGATIVE_ENABLED")), - evFilmBaseValues(ProcEventMapper::getInstance()->newEvent(FIRST, "HISTORY_MSG_FILMNEGATIVE_FILMBASE")), - filmBaseValues({0.f, 0.f, 0.f}), + NEUTRAL_TEMP(rtengine::ColorTemp(1., 1., 1., 1.)), + evFilmNegativeExponents(ProcEventMapper::getInstance()->newEvent(ALLNORAW, "HISTORY_MSG_FILMNEGATIVE_VALUES")), + evFilmNegativeEnabled(ProcEventMapper::getInstance()->newEvent(ALLNORAW, "HISTORY_MSG_FILMNEGATIVE_ENABLED")), + evFilmNegativeRefSpot(ProcEventMapper::getInstance()->newEvent(ALLNORAW, "HISTORY_MSG_FILMNEGATIVE_REF_SPOT")), + evFilmNegativeBalance(ProcEventMapper::getInstance()->newEvent(ALLNORAW, "HISTORY_MSG_FILMNEGATIVE_BALANCE")), + evFilmNegativeColorSpace(ProcEventMapper::getInstance()->newEvent(ALLNORAW, "HISTORY_MSG_FILMNEGATIVE_COLORSPACE")), + refInputValues({0.f, 0.f, 0.f}), + paramsUpgraded(false), fnp(nullptr), - greenExp(createExponentAdjuster(this, M("TP_FILMNEGATIVE_GREEN"), 0.3, 4, 1.5)), // master exponent (green channel) - redRatio(createExponentAdjuster(this, M("TP_FILMNEGATIVE_RED"), 0.3, 3, (2.04 / 1.5))), // ratio of red exponent to master exponent - blueRatio(createExponentAdjuster(this, M("TP_FILMNEGATIVE_BLUE"), 0.3, 3, (1.29 / 1.5))), // ratio of blue exponent to master exponent - spotgrid(Gtk::manage(new Gtk::Grid())), - spotbutton(Gtk::manage(new Gtk::ToggleButton(M("TP_FILMNEGATIVE_PICK")))), - filmBaseLabel(Gtk::manage(new Gtk::Label(M("TP_FILMNEGATIVE_FILMBASE_VALUES"), Gtk::ALIGN_START))), - filmBaseValuesLabel(Gtk::manage(new Gtk::Label("- - -"))), - filmBaseSpotButton(Gtk::manage(new Gtk::ToggleButton(M("TP_FILMNEGATIVE_FILMBASE_PICK")))) + colorSpace(Gtk::manage(new MyComboBoxText())), + greenExp(createExponentAdjuster(this, M("TP_FILMNEGATIVE_GREEN"), 0.3, 4, 0.01, 1.5)), // master exponent (green channel) + redRatio(createExponentAdjuster(this, M("TP_FILMNEGATIVE_RED"), 0.3, 5, 0.01, (2.04 / 1.5))), // ratio of red exponent to master exponent + blueRatio(createExponentAdjuster(this, M("TP_FILMNEGATIVE_BLUE"), 0.3, 5, 0.01, (1.29 / 1.5))), // ratio of blue exponent to master exponent + spotButton(Gtk::manage(new Gtk::ToggleButton(M("TP_FILMNEGATIVE_PICK")))), + refInputLabel(Gtk::manage(new Gtk::Label(Glib::ustring::compose(M("TP_FILMNEGATIVE_REF_LABEL"), "- - -")))), + refSpotButton(Gtk::manage(new Gtk::ToggleButton(M("TP_FILMNEGATIVE_REF_PICK")))), + outputLevel(createLevelAdjuster(this, M("TP_FILMNEGATIVE_OUT_LEVEL"))), // ref level + greenBalance(createBalanceAdjuster(this, M("TP_FILMNEGATIVE_GREENBALANCE"), -3.0, 3.0, 0.0, "circle-magenta-small.png", "circle-green-small.png")), // green balance + blueBalance(createBalanceAdjuster(this, M("TP_FILMNEGATIVE_BLUEBALANCE"), -3.0, 3.0, 0.0, "circle-blue-small.png", "circle-yellow-small.png")) // blue balance { - spotgrid->get_style_context()->add_class("grid-spacing"); - setExpandAlignProperties(spotgrid, true, false, Gtk::ALIGN_FILL, Gtk::ALIGN_CENTER); + setExpandAlignProperties(spotButton, true, false, Gtk::ALIGN_FILL, Gtk::ALIGN_CENTER); + spotButton->get_style_context()->add_class("independent"); + spotButton->set_tooltip_text(M("TP_FILMNEGATIVE_GUESS_TOOLTIP")); + spotButton->set_image(*Gtk::manage(new RTImage("color-picker-small.png"))); - setExpandAlignProperties(spotbutton, true, false, Gtk::ALIGN_FILL, Gtk::ALIGN_CENTER); - spotbutton->get_style_context()->add_class("independent"); - spotbutton->set_tooltip_text(M("TP_FILMNEGATIVE_GUESS_TOOLTIP")); - spotbutton->set_image(*Gtk::manage(new RTImage("color-picker-small.png"))); + refSpotButton->set_tooltip_text(M("TP_FILMNEGATIVE_REF_TOOLTIP")); - filmBaseSpotButton->set_tooltip_text(M("TP_FILMNEGATIVE_FILMBASE_TOOLTIP")); - setExpandAlignProperties(filmBaseValuesLabel, false, false, Gtk::ALIGN_START, Gtk::ALIGN_CENTER); + setExpandAlignProperties(refInputLabel, false, false, Gtk::ALIGN_START, Gtk::ALIGN_CENTER); +// refInputLabel->set_justify(Gtk::Justification::JUSTIFY_CENTER); +// refInputLabel->set_line_wrap(true); // TODO make spot size configurable ? @@ -98,30 +232,53 @@ FilmNegative::FilmNegative() : // spotsize->set_active(0); // spotsize->append ("4"); - spotgrid->attach(*spotbutton, 0, 1, 1, 1); + // spotgrid->attach(*spotButton, 0, 1, 1, 1); // spotgrid->attach (*slab, 1, 0, 1, 1); // spotgrid->attach (*wbsizehelper, 2, 0, 1, 1); + colorSpace->append(M("TP_FILMNEGATIVE_COLORSPACE_INPUT")); + colorSpace->append(M("TP_FILMNEGATIVE_COLORSPACE_WORKING")); + setExpandAlignProperties(colorSpace, true, false, Gtk::ALIGN_FILL, Gtk::ALIGN_CENTER); + colorSpace->set_tooltip_markup(M("TP_FILMNEGATIVE_COLORSPACE_TOOLTIP")); + + Gtk::Grid* csGrid = Gtk::manage(new Gtk::Grid()); + Gtk::Label* csLabel = Gtk::manage(new Gtk::Label(M("TP_FILMNEGATIVE_COLORSPACE"))); + csGrid->attach(*csLabel, 0, 0, 1, 1); + csGrid->attach(*colorSpace, 1, 0, 1, 1); + + pack_start(*csGrid); + + colorSpace->set_active((int)ColorSpace::WORKING); + colorSpace->signal_changed().connect(sigc::mem_fun(*this, &FilmNegative::colorSpaceChanged)); + colorSpace->show(); + pack_start(*greenExp, Gtk::PACK_SHRINK, 0); pack_start(*redRatio, Gtk::PACK_SHRINK, 0); pack_start(*blueRatio, Gtk::PACK_SHRINK, 0); - pack_start(*spotgrid, Gtk::PACK_SHRINK, 0); + pack_start(*spotButton, Gtk::PACK_SHRINK, 0); + +// pack_start(*oldMethod, Gtk::PACK_SHRINK, 0); Gtk::HSeparator* const sep = Gtk::manage(new Gtk::HSeparator()); sep->get_style_context()->add_class("grid-row-separator"); pack_start(*sep, Gtk::PACK_SHRINK, 0); - Gtk::Grid* const fbGrid = Gtk::manage(new Gtk::Grid()); - fbGrid->attach(*filmBaseLabel, 0, 0, 1, 1); - fbGrid->attach(*filmBaseValuesLabel, 1, 0, 1, 1); - pack_start(*fbGrid, Gtk::PACK_SHRINK, 0); +// Gtk::Grid* const fbGrid = Gtk::manage(new Gtk::Grid()); +// fbGrid->attach(*refInputLabel, 0, 0, 1, 1); +// fbGrid->attach(*filmBaseValuesLabel, 1, 0, 1, 1); +// pack_start(*fbGrid, Gtk::PACK_SHRINK, 0); + pack_start(*refInputLabel, Gtk::PACK_SHRINK, 0); - pack_start(*filmBaseSpotButton, Gtk::PACK_SHRINK, 0); + pack_start(*outputLevel, Gtk::PACK_SHRINK, 0); + pack_start(*blueBalance, Gtk::PACK_SHRINK, 0); + pack_start(*greenBalance, Gtk::PACK_SHRINK, 0); - spotbutton->signal_toggled().connect(sigc::mem_fun(*this, &FilmNegative::editToggled)); + pack_start(*refSpotButton, Gtk::PACK_SHRINK, 0); + + spotButton->signal_toggled().connect(sigc::mem_fun(*this, &FilmNegative::editToggled)); // spotsize->signal_changed().connect( sigc::mem_fun(*this, &WhiteBalance::spotSizeChanged) ); - filmBaseSpotButton->signal_toggled().connect(sigc::mem_fun(*this, &FilmNegative::baseSpotToggled)); + refSpotButton->signal_toggled().connect(sigc::mem_fun(*this, &FilmNegative::refSpotToggled)); // Editing geometry; create the spot rectangle Rectangle* const spotRect = new Rectangle(); @@ -139,6 +296,8 @@ FilmNegative::FilmNegative() : FilmNegative::~FilmNegative() { + idle_register.destroy(); + for (auto geometry : visibleGeometry) { delete geometry; } @@ -148,6 +307,26 @@ FilmNegative::~FilmNegative() } } + +void FilmNegative::readOutputSliders(RGB &refOut) +{ + temp2rgb(fromAdjuster(outputLevel->getValue()), + NEUTRAL_TEMP.getTemp() / std::pow(2., blueBalance->getValue()), + NEUTRAL_TEMP.getGreen() / std::pow(2., greenBalance->getValue()), + refOut); +} + +void FilmNegative::writeOutputSliders(const RGB &refOut) +{ + double outLev, cTemp, green; + rgb2temp(refOut, outLev, cTemp, green); + + outputLevel->setValue(toAdjuster(outLev)); + blueBalance->setValue(std::log2(NEUTRAL_TEMP.getTemp() / cTemp)); + greenBalance->setValue(std::log2(NEUTRAL_TEMP.getGreen() / green)); +} + + void FilmNegative::read(const rtengine::procparams::ProcParams* pp, const ParamsEdited* pedited) { disableListener(); @@ -156,27 +335,54 @@ void FilmNegative::read(const rtengine::procparams::ProcParams* pp, const Params redRatio->setEditedState(pedited->filmNegative.redRatio ? Edited : UnEdited); greenExp->setEditedState(pedited->filmNegative.greenExp ? Edited : UnEdited); blueRatio->setEditedState(pedited->filmNegative.blueRatio ? Edited : UnEdited); + outputLevel->setEditedState(pedited->filmNegative.refOutput ? Edited : UnEdited); + greenBalance->setEditedState(pedited->filmNegative.refOutput ? Edited : UnEdited); + blueBalance->setEditedState(pedited->filmNegative.refOutput ? Edited : UnEdited); set_inconsistent(multiImage && !pedited->filmNegative.enabled); } setEnabled(pp->filmNegative.enabled); + + colorSpace->set_active(CLAMP((int)pp->filmNegative.colorSpace, 0, 1)); redRatio->setValue(pp->filmNegative.redRatio); greenExp->setValue(pp->filmNegative.greenExp); blueRatio->setValue(pp->filmNegative.blueRatio); - filmBaseValues[0] = pp->filmNegative.redBase; - filmBaseValues[1] = pp->filmNegative.greenBase; - filmBaseValues[2] = pp->filmNegative.blueBase; + refInputValues = pp->filmNegative.refInput; - // If base values are not set in params, estimated values will be passed in later + // If reference input values are not set in params, estimated values will be passed in later // (after processing) via FilmNegListener - filmBaseValuesLabel->set_text(formatBaseValues(filmBaseValues)); + refInputLabel->set_markup( + Glib::ustring::compose(M("TP_FILMNEGATIVE_REF_LABEL"), fmt(refInputValues))); + + if (pp->filmNegative.backCompat == BackCompat::CURRENT) { + outputLevel->show(); + blueBalance->show(); + greenBalance->show(); + } else { + outputLevel->hide(); + blueBalance->hide(); + greenBalance->hide(); + } + + // If reference output values are not set in params, set the default output + // chosen for median estimation: gray 1/24th of max + if (pp->filmNegative.refOutput.r <= 0) { + float gray = rtengine::MAXVALF / 24.f; + writeOutputSliders({gray, gray, gray}); + } else { + writeOutputSliders(pp->filmNegative.refOutput); + } enableListener(); } void FilmNegative::write(rtengine::procparams::ProcParams* pp, ParamsEdited* pedited) { + if (colorSpace->get_active_row_number() != 3) { // UNCHANGED entry, see setBatchMode + pp->filmNegative.colorSpace = rtengine::procparams::FilmNegativeParams::ColorSpace(colorSpace->get_active_row_number()); + } + pp->filmNegative.redRatio = redRatio->getValue(); pp->filmNegative.greenExp = greenExp->getValue(); pp->filmNegative.blueRatio = blueRatio->getValue(); @@ -184,18 +390,23 @@ void FilmNegative::write(rtengine::procparams::ProcParams* pp, ParamsEdited* ped pp->filmNegative.enabled = getEnabled(); if (pedited) { + pedited->filmNegative.colorSpace = colorSpace->get_active_row_number() != 3; // UNCHANGED entry, see setBatchMode pedited->filmNegative.redRatio = redRatio->getEditedState(); pedited->filmNegative.greenExp = greenExp->getEditedState(); pedited->filmNegative.blueRatio = blueRatio->getEditedState(); - pedited->filmNegative.baseValues = filmBaseValues[0] != pp->filmNegative.redBase - || filmBaseValues[1] != pp->filmNegative.greenBase - || filmBaseValues[2] != pp->filmNegative.blueBase; + pedited->filmNegative.refOutput = outputLevel->getEditedState() || greenBalance->getEditedState() || blueBalance->getEditedState(); + // In batch mode, make sure refinput is always updated together with the balance sliders + pedited->filmNegative.refInput = pedited->filmNegative.refOutput || (refInputValues != pp->filmNegative.refInput); pedited->filmNegative.enabled = !get_inconsistent(); } - pp->filmNegative.redBase = filmBaseValues[0]; - pp->filmNegative.greenBase = filmBaseValues[1]; - pp->filmNegative.blueBase = filmBaseValues[2]; + pp->filmNegative.refInput = refInputValues; + + readOutputSliders(pp->filmNegative.refOutput); + + if (paramsUpgraded) { + pp->filmNegative.backCompat = BackCompat::CURRENT; + } } @@ -205,44 +416,70 @@ void FilmNegative::setDefaults(const rtengine::procparams::ProcParams* defParams greenExp->setValue(defParams->filmNegative.greenExp); blueRatio->setValue(defParams->filmNegative.blueRatio); + float gray = rtengine::MAXVALF / 24.f; + writeOutputSliders({gray, gray, gray}); + if (pedited) { redRatio->setDefaultEditedState(pedited->filmNegative.redRatio ? Edited : UnEdited); greenExp->setDefaultEditedState(pedited->filmNegative.greenExp ? Edited : UnEdited); blueRatio->setDefaultEditedState(pedited->filmNegative.blueRatio ? Edited : UnEdited); + + outputLevel->setDefaultEditedState(pedited->filmNegative.refOutput ? Edited : UnEdited); + greenBalance->setDefaultEditedState(pedited->filmNegative.refOutput ? Edited : UnEdited); + blueBalance->setDefaultEditedState(pedited->filmNegative.refOutput ? Edited : UnEdited); } else { redRatio->setDefaultEditedState(Irrelevant); greenExp->setDefaultEditedState(Irrelevant); blueRatio->setDefaultEditedState(Irrelevant); + outputLevel->setDefaultEditedState(Irrelevant); + greenBalance->setDefaultEditedState(Irrelevant); + blueBalance->setDefaultEditedState(Irrelevant); } } void FilmNegative::setBatchMode(bool batchMode) { + ToolPanel::setBatchMode(batchMode); + if (batchMode) { - removeIfThere(this, spotgrid, false); - removeIfThere(this, filmBaseSpotButton, false); - ToolPanel::setBatchMode(batchMode); + removeIfThere(this, spotButton, false); + removeIfThere(this, refSpotButton, false); + colorSpace->append(M("GENERAL_UNCHANGED")); + colorSpace->set_active_text(M("GENERAL_UNCHANGED")); redRatio->showEditedCB(); greenExp->showEditedCB(); blueRatio->showEditedCB(); + removeIfThere(this, refInputLabel, false); + removeIfThere(this, outputLevel, false); + removeIfThere(this, greenBalance, false); + removeIfThere(this, blueBalance, false); } } void FilmNegative::adjusterChanged(Adjuster* a, double newval) { - if (listener) { + if (listener && getEnabled()) { if (a == redRatio || a == greenExp || a == blueRatio) { - if (getEnabled()) { - listener->panelChanged( - evFilmNegativeExponents, - Glib::ustring::compose( - "Ref=%1\nR=%2\nB=%3", - greenExp->getValue(), - redRatio->getValue(), - blueRatio->getValue() - ) - ); - } + listener->panelChanged( + evFilmNegativeExponents, + Glib::ustring::compose( + "Ref=%1\nR=%2\nB=%3", + greenExp->getValue(), + redRatio->getValue(), + blueRatio->getValue() + ) + ); + } else if (a == outputLevel || a == greenBalance || a == blueBalance) { + + listener->panelChanged( + evFilmNegativeBalance, + Glib::ustring::compose( + "Lev=%1 G=%2 B=%3", + outputLevel->getValue(), + greenBalance->getValue(), + blueBalance->getValue() + ) + ); } } } @@ -260,10 +497,37 @@ void FilmNegative::enabledChanged() } } -void FilmNegative::filmBaseValuesChanged(std::array rgb) +void FilmNegative::colorSpaceChanged() { - filmBaseValues = rgb; - filmBaseValuesLabel->set_text(formatBaseValues(filmBaseValues)); + if (listener) { + listener->panelChanged(evFilmNegativeColorSpace, colorSpace->get_active_text()); + } +} + +void FilmNegative::filmRefValuesChanged(const RGB &refInput, const RGB &refOutput) +{ + + idle_register.add( + [this, refInput, refOutput]() -> bool { + refInputValues = refInput; + paramsUpgraded = true; + + disableListener(); + + refInputLabel->set_markup( + Glib::ustring::compose(M("TP_FILMNEGATIVE_REF_LABEL"), fmt(refInputValues))); + + writeOutputSliders(refOutput); + + outputLevel->show(); + blueBalance->show(); + greenBalance->show(); + + enableListener(); + return false; + } + ); + } void FilmNegative::setFilmNegProvider(FilmNegProvider* provider) @@ -297,7 +561,7 @@ bool FilmNegative::button1Pressed(int modifierKey) EditSubscriber::action = EditSubscriber::Action::NONE; if (listener) { - if (spotbutton->get_active()) { + if (spotButton->get_active()) { refSpotCoords.push_back(provider->posImage); @@ -305,17 +569,23 @@ bool FilmNegative::button1Pressed(int modifierKey) // User has selected 2 reference gray spots. Calculating new exponents // from channel values and updating parameters. - std::array newExps; + RGB ref1, ref2, dummy; + + if (fnp->getFilmNegativeSpot(refSpotCoords[0], 32, ref1, dummy) && + fnp->getFilmNegativeSpot(refSpotCoords[1], 32, ref2, dummy)) { - if (fnp->getFilmNegativeExponents(refSpotCoords[0], refSpotCoords[1], newExps)) { disableListener(); + + RGB newExps = getFilmNegativeExponents(ref1, ref2); + // Leaving green exponent unchanged, setting red and blue exponents based on // the ratios between newly calculated exponents. - redRatio->setValue(newExps[0] / newExps[1]); - blueRatio->setValue(newExps[2] / newExps[1]); + redRatio->setValue(newExps.r / newExps.g); + blueRatio->setValue(newExps.b / newExps.g); + enableListener(); - if (listener && getEnabled()) { + if (getEnabled()) { listener->panelChanged( evFilmNegativeExponents, Glib::ustring::compose( @@ -326,32 +596,37 @@ bool FilmNegative::button1Pressed(int modifierKey) ) ); } + } switchOffEditMode(); + } - } else if (filmBaseSpotButton->get_active()) { - std::array newBaseLev; + } else if (refSpotButton->get_active()) { - if (fnp->getRawSpotValues(provider->posImage, 32, newBaseLev)) { - disableListener(); + RGB refOut; + fnp->getFilmNegativeSpot(provider->posImage, 32, refInputValues, refOut); - filmBaseValues = newBaseLev; + disableListener(); - enableListener(); + float gray = rtengine::Color::rgbLuminance(refOut.r, refOut.g, refOut.b); + writeOutputSliders({gray, gray, gray}); - const Glib::ustring vs = formatBaseValues(filmBaseValues); + refInputLabel->set_text( + Glib::ustring::compose(M("TP_FILMNEGATIVE_REF_LABEL"), fmt(refInputValues))); - filmBaseValuesLabel->set_text(vs); + enableListener(); - if (listener && getEnabled()) { - listener->panelChanged(evFilmBaseValues, vs); - } - } + listener->panelChanged( + evFilmNegativeRefSpot, + Glib::ustring::compose( + "%1, %2, %3", + round(refInputValues.r), round(refInputValues.g), round(refInputValues.b) + ) + ); - switchOffEditMode(); } } @@ -364,19 +639,26 @@ bool FilmNegative::button1Released() return true; } +bool FilmNegative::button3Pressed(int modifierKey) +{ + EditSubscriber::action = EditSubscriber::Action::NONE; + switchOffEditMode(); + return true; +} + void FilmNegative::switchOffEditMode() { refSpotCoords.clear(); unsubscribe(); - spotbutton->set_active(false); - filmBaseSpotButton->set_active(false); + spotButton->set_active(false); + refSpotButton->set_active(false); } void FilmNegative::editToggled() { - if (spotbutton->get_active()) { + if (spotButton->get_active()) { - filmBaseSpotButton->set_active(false); + refSpotButton->set_active(false); refSpotCoords.clear(); subscribe(); @@ -394,11 +676,12 @@ void FilmNegative::editToggled() } } -void FilmNegative::baseSpotToggled() + +void FilmNegative::refSpotToggled() { - if (filmBaseSpotButton->get_active()) { + if (refSpotButton->get_active()) { - spotbutton->set_active(false); + spotButton->set_active(false); refSpotCoords.clear(); subscribe(); @@ -410,6 +693,7 @@ void FilmNegative::baseSpotToggled() // This is to make sure the getCursor() call is fired everywhere. Rectangle* const imgRect = static_cast(mouseOverGeometry.at(0)); imgRect->setXYWH(0, 0, w, h); + } else { refSpotCoords.clear(); unsubscribe(); diff --git a/rtgui/filmnegative.h b/rtgui/filmnegative.h index 0810a8c57..e27a38e09 100644 --- a/rtgui/filmnegative.h +++ b/rtgui/filmnegative.h @@ -27,13 +27,21 @@ #include "guiutils.h" #include "toolpanel.h" +#include "../rtengine/colortemp.h" + +namespace +{ +using RGB = rtengine::procparams::FilmNegativeParams::RGB; +using ColorSpace = rtengine::procparams::FilmNegativeParams::ColorSpace; +using BackCompat = rtengine::procparams::FilmNegativeParams::BackCompat; +} + class FilmNegProvider { public: virtual ~FilmNegProvider() = default; - virtual bool getFilmNegativeExponents(rtengine::Coord spotA, rtengine::Coord spotB, std::array& newExps) = 0; - virtual bool getRawSpotValues(rtengine::Coord spot, int spotSize, std::array& rawValues) = 0; + virtual bool getFilmNegativeSpot(rtengine::Coord spot, int spotSize, RGB &refInput, RGB &refOutput) = 0; }; class FilmNegative final : @@ -54,8 +62,9 @@ public: void adjusterChanged(Adjuster* a, double newval) override; void enabledChanged() override; + void colorSpaceChanged(); - void filmBaseValuesChanged(std::array rgb) override; + void filmRefValuesChanged(const RGB &refInput, const RGB &refOutput) override; void setFilmNegProvider(FilmNegProvider* provider); @@ -66,31 +75,47 @@ public: bool mouseOver(int modifierKey) override; bool button1Pressed(int modifierKey) override; bool button1Released() override; + bool button3Pressed(int modifierKey) override; void switchOffEditMode() override; private: void editToggled(); - void baseSpotToggled(); + void refSpotToggled(); + + void readOutputSliders(RGB &refOutput); + void writeOutputSliders(const RGB &refOutput); + + // ColorTemp value corresponding to neutral RGB multipliers (1,1,1). Should be around 6500K. + const rtengine::ColorTemp NEUTRAL_TEMP; const rtengine::ProcEvent evFilmNegativeExponents; const rtengine::ProcEvent evFilmNegativeEnabled; - const rtengine::ProcEvent evFilmBaseValues; + const rtengine::ProcEvent evFilmNegativeRefSpot; + const rtengine::ProcEvent evFilmNegativeBalance; + const rtengine::ProcEvent evFilmNegativeColorSpace; std::vector refSpotCoords; - std::array filmBaseValues; + RGB refInputValues; + bool paramsUpgraded; FilmNegProvider* fnp; + MyComboBoxText* const colorSpace; + Adjuster* const greenExp; Adjuster* const redRatio; Adjuster* const blueRatio; - Gtk::Grid* const spotgrid; - Gtk::ToggleButton* const spotbutton; + Gtk::ToggleButton* const spotButton; - Gtk::Label* const filmBaseLabel; - Gtk::Label* const filmBaseValuesLabel; - Gtk::ToggleButton* const filmBaseSpotButton; + Gtk::Label* const refInputLabel; + Gtk::ToggleButton* const refSpotButton; + + Adjuster* const outputLevel; + Adjuster* const greenBalance; + Adjuster* const blueBalance; + + IdleRegister idle_register; }; diff --git a/rtgui/paramsedited.cc b/rtgui/paramsedited.cc index 2543a25f7..611086ef3 100644 --- a/rtgui/paramsedited.cc +++ b/rtgui/paramsedited.cc @@ -676,7 +676,9 @@ void ParamsEdited::set(bool v) filmNegative.redRatio = v; filmNegative.greenExp = v; filmNegative.blueRatio = v; - filmNegative.baseValues = v; + filmNegative.refInput = v; + filmNegative.refOutput = v; + filmNegative.colorSpace = v; raw.preprocessWB.mode = v; exif = v; @@ -1865,9 +1867,9 @@ void ParamsEdited::initFrom(const std::vector& filmNegative.redRatio = filmNegative.redRatio && p.filmNegative.redRatio == other.filmNegative.redRatio; filmNegative.greenExp = filmNegative.greenExp && p.filmNegative.greenExp == other.filmNegative.greenExp; filmNegative.blueRatio = filmNegative.blueRatio && p.filmNegative.blueRatio == other.filmNegative.blueRatio; - filmNegative.baseValues = filmNegative.baseValues && p.filmNegative.redBase == other.filmNegative.redBase - && p.filmNegative.greenBase == other.filmNegative.greenBase - && p.filmNegative.blueBase == other.filmNegative.blueBase; + filmNegative.refInput = filmNegative.refInput && p.filmNegative.refInput == other.filmNegative.refInput; + filmNegative.refOutput = filmNegative.refOutput && p.filmNegative.refOutput == other.filmNegative.refOutput; + filmNegative.colorSpace = filmNegative.colorSpace && p.filmNegative.colorSpace == other.filmNegative.colorSpace; raw.preprocessWB.mode = raw.preprocessWB.mode && p.raw.preprocessWB.mode == other.raw.preprocessWB.mode; // How the hell can we handle that??? @@ -6254,12 +6256,21 @@ void ParamsEdited::combine(rtengine::procparams::ProcParams& toEdit, const rteng toEdit.filmNegative.blueRatio = mods.filmNegative.blueRatio; } - if (filmNegative.baseValues) { - toEdit.filmNegative.redBase = mods.filmNegative.redBase; - toEdit.filmNegative.greenBase = mods.filmNegative.greenBase; - toEdit.filmNegative.blueBase = mods.filmNegative.blueBase; + if (filmNegative.refInput) { + toEdit.filmNegative.refInput = mods.filmNegative.refInput; } + if (filmNegative.refOutput) { + toEdit.filmNegative.refOutput = mods.filmNegative.refOutput; + } + + if (filmNegative.colorSpace) { + toEdit.filmNegative.colorSpace = mods.filmNegative.colorSpace; + } + + // BackCompat param cannot be managed via the GUI, and it's always copied + toEdit.filmNegative.backCompat = mods.filmNegative.backCompat; + if (raw.preprocessWB.mode) { toEdit.raw.preprocessWB.mode = mods.raw.preprocessWB.mode; } @@ -6310,7 +6321,7 @@ bool RetinexParamsEdited::isUnchanged() const bool FilmNegativeParamsEdited::isUnchanged() const { - return enabled && redRatio && greenExp && blueRatio && baseValues; + return enabled && redRatio && greenExp && blueRatio && refInput && refOutput && colorSpace; } LocallabParamsEdited::LocallabSpotEdited::LocallabSpotEdited(bool v) : diff --git a/rtgui/paramsedited.h b/rtgui/paramsedited.h index 523999e67..bfbc00a25 100644 --- a/rtgui/paramsedited.h +++ b/rtgui/paramsedited.h @@ -1303,7 +1303,9 @@ struct FilmNegativeParamsEdited { bool redRatio; bool greenExp; bool blueRatio; - bool baseValues; + bool refInput; + bool refOutput; + bool colorSpace; bool isUnchanged() const; }; diff --git a/rtgui/partialpastedlg.cc b/rtgui/partialpastedlg.cc index 6808a2cc6..83a3b7cc0 100644 --- a/rtgui/partialpastedlg.cc +++ b/rtgui/partialpastedlg.cc @@ -355,6 +355,7 @@ PartialPasteDlg::PartialPasteDlg (const Glib::ustring &title, Gtk::Window* paren vboxes[2]->pack_start (*blackwhite, Gtk::PACK_SHRINK, 2); vboxes[2]->pack_start (*hsveq, Gtk::PACK_SHRINK, 2); vboxes[2]->pack_start (*filmSimulation, Gtk::PACK_SHRINK, 2); + vboxes[2]->pack_start (*filmNegative, Gtk::PACK_SHRINK, 2); vboxes[2]->pack_start (*softlight, Gtk::PACK_SHRINK, 2); vboxes[2]->pack_start (*rgbcurves, Gtk::PACK_SHRINK, 2); vboxes[2]->pack_start (*colortoning, Gtk::PACK_SHRINK, 2); @@ -425,7 +426,6 @@ PartialPasteDlg::PartialPasteDlg (const Glib::ustring &title, Gtk::Window* paren vboxes[8]->pack_start (*raw_caredblue, Gtk::PACK_SHRINK, 2); vboxes[8]->pack_start (*raw_ca_avoid_colourshift, Gtk::PACK_SHRINK, 2); vboxes[8]->pack_start (*Gtk::manage (new Gtk::HSeparator ()), Gtk::PACK_SHRINK, 0); - vboxes[8]->pack_start (*filmNegative, Gtk::PACK_SHRINK, 2); vboxes[8]->pack_start (*captureSharpening, Gtk::PACK_SHRINK, 2); Gtk::VBox* vbCol1 = Gtk::manage (new Gtk::VBox ()); @@ -655,7 +655,6 @@ void PartialPasteDlg::rawToggled () ConnectionBlocker raw_ca_autocorrectBlocker(raw_ca_autocorrectConn); ConnectionBlocker raw_caredblueBlocker(raw_caredblueConn); ConnectionBlocker raw_ca_avoid_colourshiftBlocker(raw_ca_avoid_colourshiftconn); - ConnectionBlocker filmNegativeBlocker(filmNegativeConn); ConnectionBlocker captureSharpeningBlocker(captureSharpeningConn); ConnectionBlocker raw_preprocwbBlocker(raw_preprocwbConn); @@ -686,7 +685,6 @@ void PartialPasteDlg::rawToggled () raw_ca_autocorrect->set_active (raw->get_active ()); raw_caredblue->set_active (raw->get_active ()); raw_ca_avoid_colourshift->set_active (raw->get_active ()); - filmNegative->set_active (raw->get_active()); captureSharpening->set_active (raw->get_active()); raw_preprocwb->set_active (raw->get_active()); } @@ -764,6 +762,7 @@ void PartialPasteDlg::colorToggled () ConnectionBlocker chmixerbwBlocker(chmixerbwConn); ConnectionBlocker hsveqBlocker(hsveqConn); ConnectionBlocker filmSimulationBlocker(filmSimulationConn); + ConnectionBlocker filmNegativeBlocker(filmNegativeConn); ConnectionBlocker softlightBlocker(softlightConn); ConnectionBlocker rgbcurvesBlocker(rgbcurvesConn); ConnectionBlocker colortoningBlocker(colortoningConn); @@ -776,6 +775,7 @@ void PartialPasteDlg::colorToggled () blackwhite->set_active (color->get_active ()); hsveq->set_active (color->get_active ()); filmSimulation->set_active (color->get_active ()); + filmNegative->set_active (color->get_active()); softlight->set_active (color->get_active ()); rgbcurves->set_active (color->get_active ()); colortoning->set_active(color->get_active ()); @@ -1178,7 +1178,9 @@ void PartialPasteDlg::applyPaste (rtengine::procparams::ProcParams* dstPP, Param filterPE.filmNegative.redRatio = falsePE.filmNegative.redRatio; filterPE.filmNegative.greenExp = falsePE.filmNegative.greenExp; filterPE.filmNegative.blueRatio = falsePE.filmNegative.blueRatio; - filterPE.filmNegative.baseValues = falsePE.filmNegative.baseValues; + filterPE.filmNegative.refInput = falsePE.filmNegative.refInput; + filterPE.filmNegative.refOutput = falsePE.filmNegative.refOutput; + filterPE.filmNegative.colorSpace = falsePE.filmNegative.colorSpace; } if (!captureSharpening->get_active ()) { diff --git a/rtgui/toolpanelcoord.cc b/rtgui/toolpanelcoord.cc index cd24897a5..0b4ffc24b 100644 --- a/rtgui/toolpanelcoord.cc +++ b/rtgui/toolpanelcoord.cc @@ -99,7 +99,7 @@ ToolPanelCoordinator::ToolPanelCoordinator (bool batch) : ipc (nullptr), favorit bayerrawexposure = Gtk::manage(new BayerRAWExposure()); xtransrawexposure = Gtk::manage(new XTransRAWExposure()); fattal = Gtk::manage(new FattalToneMapping()); - filmNegative = Gtk::manage (new FilmNegative ()); + filmNegative = Gtk::manage (new FilmNegative()); pdSharpening = Gtk::manage (new PdSharpening()); // So Demosaic, Line noise filter, Green Equilibration, Ca-Correction (garder le nom de section identique!) and Black-Level will be moved in a "Bayer sensor" tool, // and a separate Demosaic and Black Level tool will be created in an "X-Trans sensor" tool @@ -123,6 +123,7 @@ ToolPanelCoordinator::ToolPanelCoordinator (bool batch) : ipc (nullptr), favorit addfavoritePanel (detailsPanel, sharpenMicro); addfavoritePanel (colorPanel, hsvequalizer); addfavoritePanel (colorPanel, filmSimulation); + addfavoritePanel (colorPanel, filmNegative); addfavoritePanel (colorPanel, softlight); addfavoritePanel (colorPanel, rgbcurves); addfavoritePanel (colorPanel, colortoning); @@ -165,7 +166,6 @@ ToolPanelCoordinator::ToolPanelCoordinator (bool batch) : ipc (nullptr), favorit addfavoritePanel (rawPanel, preprocess); addfavoritePanel (rawPanel, darkframe); addfavoritePanel (rawPanel, flatfield); - addfavoritePanel (rawPanel, filmNegative); addfavoritePanel (rawPanel, pdSharpening); int favoriteCount = 0; @@ -286,7 +286,7 @@ ToolPanelCoordinator::ToolPanelCoordinator (bool batch) : ipc (nullptr), favorit distortion->setLensGeomListener(this); crop->setCropPanelListener(this); icm->setICMPanelListener(this); - filmNegative->setFilmNegProvider (this); + filmNegative->setFilmNegProvider(this); toolBar = new ToolBar(); toolBar->setToolBarListener(this); @@ -367,7 +367,6 @@ void ToolPanelCoordinator::imageTypeChanged(bool isRaw, bool isBayer, bool isXtr preprocessWB->FoldableToolPanel::show(); preprocess->FoldableToolPanel::show(); flatfield->FoldableToolPanel::show(); - filmNegative->FoldableToolPanel::show(); pdSharpening->FoldableToolPanel::show(); retinex->FoldableToolPanel::setGrayedOut(false); return false; @@ -388,7 +387,6 @@ void ToolPanelCoordinator::imageTypeChanged(bool isRaw, bool isBayer, bool isXtr preprocessWB->FoldableToolPanel::show(); preprocess->FoldableToolPanel::show(); flatfield->FoldableToolPanel::show(); - filmNegative->FoldableToolPanel::show(); pdSharpening->FoldableToolPanel::show(); retinex->FoldableToolPanel::setGrayedOut(false); return false; @@ -409,7 +407,6 @@ void ToolPanelCoordinator::imageTypeChanged(bool isRaw, bool isBayer, bool isXtr preprocessWB->FoldableToolPanel::hide(); preprocess->FoldableToolPanel::hide(); flatfield->FoldableToolPanel::show(); - filmNegative->FoldableToolPanel::hide(); pdSharpening->FoldableToolPanel::show(); retinex->FoldableToolPanel::setGrayedOut(false); return false; @@ -430,7 +427,6 @@ void ToolPanelCoordinator::imageTypeChanged(bool isRaw, bool isBayer, bool isXtr preprocessWB->FoldableToolPanel::hide(); preprocess->FoldableToolPanel::hide(); flatfield->FoldableToolPanel::hide(); - filmNegative->FoldableToolPanel::hide(); pdSharpening->FoldableToolPanel::hide(); retinex->FoldableToolPanel::setGrayedOut(false); return false; @@ -452,7 +448,6 @@ void ToolPanelCoordinator::imageTypeChanged(bool isRaw, bool isBayer, bool isXtr preprocessWB->FoldableToolPanel::hide(); preprocess->FoldableToolPanel::hide(); flatfield->FoldableToolPanel::hide(); - filmNegative->FoldableToolPanel::hide(); pdSharpening->FoldableToolPanel::hide(); retinex->FoldableToolPanel::setGrayedOut(true); return false; @@ -613,7 +608,7 @@ void ToolPanelCoordinator::profileChange( lParams[1] = *mergedParams; pe.initFrom(lParams); - filterRawRefresh = pe.raw.isUnchanged() && pe.lensProf.isUnchanged() && pe.retinex.isUnchanged() && pe.filmNegative.isUnchanged() && pe.pdsharpening.isUnchanged(); + filterRawRefresh = pe.raw.isUnchanged() && pe.lensProf.isUnchanged() && pe.retinex.isUnchanged() && pe.pdsharpening.isUnchanged(); } *params = *mergedParams; @@ -732,7 +727,7 @@ void ToolPanelCoordinator::initImage(rtengine::StagedImageProcessor* ipc_, bool ipc->setSizeListener(resize); ipc->setLocallabListener(locallab); ipc->setImageTypeListener(this); - ipc->setFilmNegListener (filmNegative); + ipc->setFilmNegListener(filmNegative); flatfield->setShortcutPath(Glib::path_get_dirname(ipc->getInitialImage()->getFileName())); icm->setRawMeta(raw, (const rtengine::FramesData*)pMetaData); @@ -1280,12 +1275,7 @@ void ToolPanelCoordinator::setEditProvider(EditDataProvider *provider) } } -bool ToolPanelCoordinator::getFilmNegativeExponents(rtengine::Coord spotA, rtengine::Coord spotB, std::array& newExps) +bool ToolPanelCoordinator::getFilmNegativeSpot(rtengine::Coord spot, int spotSize, RGB &refInput, RGB &refOutput) { - return ipc && ipc->getFilmNegativeExponents(spotA.x, spotA.y, spotB.x, spotB.y, newExps); -} - -bool ToolPanelCoordinator::getRawSpotValues(rtengine::Coord spot, int spotSize, std::array& rawValues) -{ - return ipc && ipc->getRawSpotValues(spot.x, spot.y, spotSize, rawValues); + return ipc && ipc->getFilmNegativeSpot(spot.x, spot.y, spotSize, refInput, refOutput); } diff --git a/rtgui/toolpanelcoord.h b/rtgui/toolpanelcoord.h index 0dcee59eb..b39906459 100644 --- a/rtgui/toolpanelcoord.h +++ b/rtgui/toolpanelcoord.h @@ -308,8 +308,7 @@ public: Glib::ustring GetCurrentImageFilePath() override; // FilmNegProvider interface - bool getFilmNegativeExponents(rtengine::Coord spotA, rtengine::Coord spotB, std::array& newExps) override; - bool getRawSpotValues(rtengine::Coord spot, int spotSize, std::array& rawValues) override; + bool getFilmNegativeSpot(rtengine::Coord spot, int spotSize, RGB &refInput, RGB &refOutput) override; // rotatelistener interface void straightenRequested () override;