diff --git a/rtdata/languages/default b/rtdata/languages/default index 64af97689..f33608903 100644 --- a/rtdata/languages/default +++ b/rtdata/languages/default @@ -752,6 +752,7 @@ 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_FILMNEGATIVE_ENABLED;Film Negative +HISTORY_MSG_FILMNEGATIVE_FILMBASE;Film base color HISTORY_MSG_FILMNEGATIVE_VALUES;Film negative values HISTORY_MSG_HISTMATCHING;Auto-matched tone curve HISTORY_MSG_ICM_OUTPUT_PRIMARIES;Output - Primaries @@ -1693,6 +1694,9 @@ 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_LABEL;Film Negative diff --git a/rtengine/filmnegativeproc.cc b/rtengine/filmnegativeproc.cc index c7de3c483..6d4fe1ad6 100644 --- a/rtengine/filmnegativeproc.cc +++ b/rtengine/filmnegativeproc.cc @@ -35,6 +35,9 @@ namespace { +using rtengine::ST_BAYER; +using rtengine::ST_FUJI_XTRANS; +using rtengine::settings; bool channelsAvg( const rtengine::RawImage* ri, @@ -43,17 +46,16 @@ bool channelsAvg( const float* cblacksom, rtengine::Coord spotPos, int spotSize, - const rtengine::procparams::FilmNegativeParams& params, std::array& avgs ) { avgs = {}; // Channel averages - if (ri->getSensorType() != rtengine::ST_BAYER && ri->getSensorType() != rtengine::ST_FUJI_XTRANS) { + if (ri->getSensorType() != ST_BAYER && ri->getSensorType() != ST_FUJI_XTRANS) { return false; } - if (rtengine::settings->verbose) { + if (settings->verbose) { printf("Spot coord: x=%d y=%d\n", spotPos.x, spotPos.y); } @@ -69,9 +71,10 @@ bool channelsAvg( } std::array pxCount = {}; // Per-channel sample counts + for (int c = x1; c < x2; ++c) { for (int r = y1; r < y2; ++r) { - const int ch = ri->getSensorType() == rtengine::ST_BAYER ? ri->FC(r,c) : ri->XTRANSFC(r,c); + const int ch = ri->getSensorType() == ST_BAYER ? ri->FC(r, c) : ri->XTRANSFC(r, c); ++pxCount[ch]; @@ -88,6 +91,112 @@ bool channelsAvg( 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]); + } + } + } 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) @@ -104,21 +213,29 @@ bool rtengine::RawImageSource::getFilmNegativeExponents(Coord2D spotA, Coord2D s std::array clearVals; std::array denseVals; + // Get channel averages in the two spots, sampling from the original ri->data buffer. + // NOTE: rawData values might be affected by CA corection, 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, currentParams, clearVals)) { + + if (!channelsAvg(ri, W, H, cblacksom, spot, spotSize, clearVals)) { return false; } // Sample second spot transformPosition(spotB.x, spotB.y, tran, spot.x, spot.y); - if (!channelsAvg(ri, W, H, cblacksom, spot, spotSize, currentParams, denseVals)) { + + 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); + std::swap(clearVals, denseVals); } if (settings->verbose) { @@ -152,7 +269,32 @@ bool rtengine::RawImageSource::getFilmNegativeExponents(Coord2D spotA, Coord2D s return true; } -void rtengine::RawImageSource::filmNegativeProcess(const procparams::FilmNegativeParams ¶ms) +bool rtengine::RawImageSource::getRawSpotValues(Coord2D spotCoord, int spotSize, int tran, const procparams::FilmNegativeParams ¶ms, std::array& rawValues) +{ + Coord spot; + transformPosition(spotCoord.x, spotCoord.y, tran, spot.x, spot.y); + + if (settings->verbose) { + printf("Transformed coords: %d,%d\n", spot.x, spot.y); + } + + if (spotSize < 4) { + return false; + } + + // Calculate averages of raw unscaled channels + if (!channelsAvg(ri, W, H, cblacksom, spot, spotSize, rawValues)) { + return false; + } + + if (settings->verbose) { + printf("Raw spot values: R=%g, G=%g, B=%g\n", rawValues[0], rawValues[1], rawValues[2]); + } + + return true; +} + +void rtengine::RawImageSource::filmNegativeProcess(const procparams::FilmNegativeParams ¶ms, std::array& filmBaseValues) { // BENCHFUNMICRO @@ -168,92 +310,114 @@ void rtengine::RawImageSource::filmNegativeProcess(const procparams::FilmNegativ static_cast(-params.blueRatio * params.greenExp) }; - MyTime t1, t2, t3,t4, t5; - - 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 = 0; row < H; row += 5) { - const int c0 = ri->FC(row, 0); - const int c1 = ri->FC(row, 5); - int col = 0; - for (; col < W - 5; col += 10) { - cvs[c0].push_back(rawData[row][col]); - cvs[c1].push_back(rawData[row][col + 5]); - } - if (col < W) { - cvs[c0].push_back(rawData[row][col]); - } - } - } - else if (ri->getSensorType() == ST_FUJI_XTRANS) { - for (int row = 0; row < H; row += 5) { - const std::array cs = { - ri->XTRANSFC(row, 0), - ri->XTRANSFC(row, 5), - ri->XTRANSFC(row, 10), - ri->XTRANSFC(row, 15), - ri->XTRANSFC(row, 20), - ri->XTRANSFC(row, 25) - }; - int col = 0; - for (; col < W - 25; col += 30) { - for (int c = 0; c < 6; ++c) { - cvs[cs[c]].push_back(rawData[row][col + c * 5]); - } - } - for (int c = 0; col < W; col += 5, ++c) { - cvs[cs[c]].push_back(rawData[row][col]); - } - } - } - constexpr float MAX_OUT_VALUE = 65000.f; - t2.set(); + // 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 (settings->verbose) { - printf("Median vector fill loop time us: %d\n", t2.etime(t1)); + + if (rtengine::settings->verbose) { + printf("Fixed WB mults: %g %g %g\n", wb_mul[0], wb_mul[1], wb_mul[2]); } - t2.set(); + // Compensation factor to restore the non-autoWB initialGain (see RawImageSource::load) + const float autoGainComp = camInitialGain / initialGain; - std::array medians; // Channel median values - std::array mults = { - 1.f, - 1.f, - 1.f - }; // Channel normalization multipliers + std::array mults; // Channel normalization multipliers + + // 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, + // whithout any previous setting. So, estimate film base values from channel medians + + std::array medians; + + // Special value for backwards compatibility with profiles saved by RT 5.7 + const bool oldChannelScaling = filmBaseValues[0] == -1.f; + + // 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); + } + + // Un-scale rawData medians + for (int c = 0; c < 3; ++c) { + medians[c] /= scale_mul[c]; + } + + 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; + } - for (int c = 0; c < 3; ++c) { - // Find median values for each channel - if (!cvs[c].empty()) { - findMinMaxPercentile(cvs[c].data(), cvs[c].size(), 0.5f, medians[c], 0.5f, medians[c], true); - medians[c] = pow_F(rtengine::max(medians[c], 1.f), exps[c]); - // Determine the channel multiplier so that N times the median becomes 65k. This clips away - // the values in the dark border surrounding the negative (due to the film holder, for example), - // the reciprocal of which have blown up to stellar values. - mults[c] = MAX_OUT_VALUE / (medians[c] * 24.f); } } - t3.set(); + + // 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] + }; if (settings->verbose) { - printf("Sample count: %zu, %zu, %zu\n", cvs[0].size(), cvs[1].size(), cvs[2].size()); - printf("Medians: %g %g %g\n", static_cast(medians[0]), static_cast(medians[1]), static_cast(medians[2])); - printf("Computed multipliers: %g %g %g\n", static_cast(mults[0]), static_cast(mults[1]), static_cast(mults[2])); - printf("Median calc time us: %d\n", t3.etime(t2)); + printf("Input film base values: %g %g %g\n", fb[0], fb[1], fb[2]); } + 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]); + + // Determine the channel multiplier so that the film base value is 1/512th of max. + mults[c] = (MAX_OUT_VALUE / 512.f) / fb[c]; + + // 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; + + } + + 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])); + } + + constexpr float CLIP_VAL = 65535.f; - t3.set(); + MyTime t1, t2, t3; + + t1.set(); if (ri->getSensorType() == ST_BAYER) { #ifdef __SSE2__ @@ -264,6 +428,7 @@ void rtengine::RawImageSource::filmNegativeProcess(const procparams::FilmNegativ #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. @@ -274,14 +439,18 @@ void rtengine::RawImageSource::filmNegativeProcess(const procparams::FilmNegativ #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); } @@ -295,6 +464,7 @@ void rtengine::RawImageSource::filmNegativeProcess(const procparams::FilmNegativ #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. @@ -321,30 +491,34 @@ void rtengine::RawImageSource::filmNegativeProcess(const procparams::FilmNegativ 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); } } } - t4.set(); + t2.set(); if (settings->verbose) { - printf("Pow loop time us: %d\n", t4.etime(t3)); + printf("Pow loop time us: %d\n", t2.etime(t1)); } - t4.set(); + t2.set(); PixelsMap bitmapBads(W, H); @@ -354,6 +528,7 @@ void rtengine::RawImageSource::filmNegativeProcess(const procparams::FilmNegativ #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) { @@ -367,11 +542,11 @@ void rtengine::RawImageSource::filmNegativeProcess(const procparams::FilmNegativ interpolateBadPixelsBayer(bitmapBads, rawData); } - } - else if (ri->getSensorType() == ST_FUJI_XTRANS) { + } 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) { @@ -386,10 +561,10 @@ void rtengine::RawImageSource::filmNegativeProcess(const procparams::FilmNegativ } } - t5.set(); + t3.set(); if (settings->verbose) { printf("Bad pixels count: %d\n", totBP); - printf("Bad pixels interpolation time us: %d\n", t5.etime(t4)); + printf("Bad pixels interpolation time us: %d\n", t3.etime(t2)); } } diff --git a/rtengine/filmnegativethumb.cc b/rtengine/filmnegativethumb.cc index 003fab8e0..57f2601f9 100644 --- a/rtengine/filmnegativethumb.cc +++ b/rtengine/filmnegativethumb.cc @@ -18,6 +18,7 @@ */ #include +#include "colortemp.h" #include "LUT.h" #include "rtengine.h" #include "rtthumbnail.h" @@ -29,70 +30,179 @@ #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, - float &rmi, float &gmi, float &bmi -) { + 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; - // 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; - - // Channel vectors to calculate medians - std::vector rv, gv, bv; - - for (int i = 0; i < rheight; i++) { - for (int j = 0; j < rwidth; j++) { - const float r = baseImg->r(i, j); - const float g = baseImg->g(i, j); - const float b = baseImg->b(i, j); - - rsum += r; - gsum += g; - bsum += b; - - rv.push_back(r); - gv.push_back(g); - bv.push_back(b); - } - } - - 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. - rmi /= (gavg/ravg); - // gmi /= (gAvg/gAvg); green chosen as reference channel - bmi /= (gavg/bavg); - - float rmed, gmed, bmed; - 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); - - rmed = powf(rmed, rexp); - gmed = powf(gmed, gexp); - bmed = powf(bmed, bexp); + // Calculate output multipliers + float rmult, gmult, bmult; const float MAX_OUT_VALUE = 65000.f; - const float rmult = (MAX_OUT_VALUE / (rmed * 24)) ; - const float gmult = (MAX_OUT_VALUE / (gmed * 24)) ; - const float bmult = (MAX_OUT_VALUE / (bmed * 24)) ; + + // 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 channel medians: %g %g %g\n", static_cast(rmed), static_cast(gmed), static_cast(bmed)); 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); @@ -109,12 +219,15 @@ void rtengine::Thumbnail::processFilmNegative( float *bline = baseImg->b(i); int j = 0; #ifdef __SSE2__ - for (; j < rwidth - 3; j +=4) { + + 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)); diff --git a/rtengine/imagesource.h b/rtengine/imagesource.h index d1008837d..ea049d37c 100644 --- a/rtengine/imagesource.h +++ b/rtengine/imagesource.h @@ -92,8 +92,9 @@ 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) {}; + 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) {}; diff --git a/rtengine/improccoordinator.cc b/rtengine/improccoordinator.cc index 6374cb837..f68616b96 100644 --- a/rtengine/improccoordinator.cc +++ b/rtengine/improccoordinator.cc @@ -44,6 +44,22 @@ #include #endif +namespace +{ +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 { @@ -300,7 +316,15 @@ void ImProcCoordinator::updatePreviewImage(int todo, bool panningRelatedChange) ) && params->filmNegative.enabled ) { - imgsrc->filmNegativeProcess(params->filmNegative); + 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); + } } } @@ -1481,27 +1505,22 @@ bool ImProcCoordinator::getFilmNegativeExponents(int xA, int yA, int xB, int yB, { MyMutex::MyLock lock(mProcessing); - const auto xlate = - [this](int x, int y) -> Coord2D - { - 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]; - }; - const int tr = getCoarseBitMask(params->coarse); - const Coord2D p1 = xlate(xA, yA); - const Coord2D p2 = xlate(xB, yB); + 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) { diff --git a/rtengine/improccoordinator.h b/rtengine/improccoordinator.h index d7d6052e9..e17d88183 100644 --- a/rtengine/improccoordinator.h +++ b/rtengine/improccoordinator.h @@ -175,6 +175,7 @@ protected: AutoRadiusListener *pdSharpenAutoRadiusListener; FrameCountListener *frameCountListener; ImageTypeListener *imageTypeListener; + FilmNegListener *filmNegListener; AutoColorTonListener* actListener; AutoChromaListener* adnListener; @@ -281,6 +282,7 @@ public: 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; void getAutoCrop (double ratio, int &x, int &y, int &w, int &h) override; bool getHighQualComputed() override; void setHighQualComputed() override; @@ -389,6 +391,11 @@ public: imageTypeListener = itl; } + void setFilmNegListener (FilmNegListener* fnl) override + { + filmNegListener = fnl; + } + void saveInputICCReference (const Glib::ustring& fname, bool apply_wb) override; InitialImage* getInitialImage () override diff --git a/rtengine/procparams.cc b/rtengine/procparams.cc index b02f1970c..a198a6813 100644 --- a/rtengine/procparams.cc +++ b/rtengine/procparams.cc @@ -2890,7 +2890,10 @@ FilmNegativeParams::FilmNegativeParams() : enabled(false), redRatio(1.36), greenExp(1.5), - blueRatio(0.86) + blueRatio(0.86), + redBase(0), + greenBase(0), + blueBase(0) { } @@ -2898,9 +2901,12 @@ bool FilmNegativeParams::operator ==(const FilmNegativeParams& other) const { return enabled == other.enabled - && redRatio == other.redRatio + && redRatio == other.redRatio && greenExp == other.greenExp - && blueRatio == other.blueRatio; + && blueRatio == other.blueRatio + && redBase == other.redBase + && greenBase == other.greenBase + && blueBase == other.blueBase; } bool FilmNegativeParams::operator !=(const FilmNegativeParams& other) const @@ -3789,6 +3795,9 @@ 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); // EXIF change list if (!pedited || pedited->exif) { @@ -5386,6 +5395,24 @@ 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) { + 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); + if (pedited) { + pedited->filmNegative.baseValues = r || g || b; + } + } 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; + } + } } if (keyFile.has_group("MetaData")) { diff --git a/rtengine/procparams.h b/rtengine/procparams.h index 4e2b0c275..f9710b01d 100644 --- a/rtengine/procparams.h +++ b/rtengine/procparams.h @@ -1559,6 +1559,10 @@ struct FilmNegativeParams { double greenExp; double blueRatio; + double redBase; + double greenBase; + double blueBase; + FilmNegativeParams(); bool operator ==(const FilmNegativeParams& other) const; diff --git a/rtengine/rawimagesource.h b/rtengine/rawimagesource.h index 71478fbed..770c18ae3 100644 --- a/rtengine/rawimagesource.h +++ b/rtengine/rawimagesource.h @@ -124,8 +124,9 @@ 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) 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; diff --git a/rtengine/rtengine.h b/rtengine/rtengine.h index e6074d50b..028cedec9 100644 --- a/rtengine/rtengine.h +++ b/rtengine/rtengine.h @@ -439,6 +439,13 @@ public: }; +class FilmNegListener +{ +public: + virtual ~FilmNegListener() = default; + virtual void filmBaseValuesChanged(std::array rgb) = 0; +}; + /** This class represents a detailed part of the image (looking through a kind of window). * It can be created and destroyed with the appropriate members of StagedImageProcessor. * Several crops can be assigned to the same image. */ @@ -524,6 +531,8 @@ public: 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 void getAutoCrop (double ratio, int &x, int &y, int &w, int &h) = 0; virtual void saveInputICCReference (const Glib::ustring& fname, bool apply_wb) = 0; @@ -548,6 +557,7 @@ public: virtual void setRetinexListener (RetinexListener* l) = 0; virtual void setWaveletListener (WaveletListener* l) = 0; virtual void setImageTypeListener (ImageTypeListener* l) = 0; + virtual void setFilmNegListener (FilmNegListener* l) = 0; virtual void setMonitorProfile (const Glib::ustring& monitorProfile, RenderingIntent intent) = 0; virtual void getMonitorProfile (Glib::ustring& monitorProfile, RenderingIntent& intent) const = 0; diff --git a/rtengine/rtthumbnail.cc b/rtengine/rtthumbnail.cc index a063d965b..e8126be36 100644 --- a/rtengine/rtthumbnail.cc +++ b/rtengine/rtthumbnail.cc @@ -594,6 +594,8 @@ 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->gammaCorrected = true; unsigned filter = ri->get_filters(); @@ -1042,6 +1044,7 @@ Thumbnail::Thumbnail () : scaleForSave (8192), gammaCorrected (false), colorMatrix{}, + scaleGain (1.0), isRaw (true) { } @@ -1179,7 +1182,7 @@ 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, rmi, gmi, bmi); + processFilmNegative(params, baseImg, rwidth, rheight); } if (params.coarse.rotate) { @@ -2122,6 +2125,10 @@ bool Thumbnail::readData (const Glib::ustring& fname) colorMatrix[i][j] = cm[ix++]; } } + + if (keyFile.has_key ("LiveThumbData", "ScaleGain")) { + scaleGain = keyFile.get_double ("LiveThumbData", "ScaleGain"); + } } return true; @@ -2173,6 +2180,7 @@ bool Thumbnail::writeData (const Glib::ustring& fname) keyFile.set_boolean ("LiveThumbData", "GammaCorrected", gammaCorrected); Glib::ArrayHandle cm ((double*)colorMatrix, 9, Glib::OWNERSHIP_NONE); keyFile.set_double_list ("LiveThumbData", "ColorMatrix", cm); + keyFile.set_double ("LiveThumbData", "ScaleGain", scaleGain); keyData = keyFile.to_data (); diff --git a/rtengine/rtthumbnail.h b/rtengine/rtthumbnail.h index ccca1c582..a0033d35f 100644 --- a/rtengine/rtthumbnail.h +++ b/rtengine/rtthumbnail.h @@ -75,8 +75,9 @@ class Thumbnail int scaleForSave; bool gammaCorrected; double colorMatrix[3][3]; + double scaleGain; - void processFilmNegative(const procparams::ProcParams& params, const Imagefloat* baseImg, int rwidth, int rheight, float &rmi, float &gmi, float &bmi); + void processFilmNegative(const procparams::ProcParams& params, const Imagefloat* baseImg, int rwidth, int rheight); public: diff --git a/rtengine/simpleprocess.cc b/rtengine/simpleprocess.cc index 14df16550..14a594ac2 100644 --- a/rtengine/simpleprocess.cc +++ b/rtengine/simpleprocess.cc @@ -221,7 +221,12 @@ private: // After preprocess, run film negative processing if enabled if ((imgsrc->getSensorType() == ST_BAYER || (imgsrc->getSensorType() == ST_FUJI_XTRANS)) && params.filmNegative.enabled) { - imgsrc->filmNegativeProcess (params.filmNegative); + 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) { diff --git a/rtgui/filmnegative.cc b/rtgui/filmnegative.cc index 90cedf148..1b105a3ec 100644 --- a/rtgui/filmnegative.cc +++ b/rtgui/filmnegative.cc @@ -44,6 +44,17 @@ Adjuster* createExponentAdjuster(AdjusterListener* listener, const Glib::ustring return adj; } +Glib::ustring formatBaseValues(const std::array& rgb) +{ + if (rgb[0] <= 0.f && rgb[1] <= 0.f && rgb[2] <= 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]); + } +} + } FilmNegative::FilmNegative() : @@ -51,12 +62,17 @@ FilmNegative::FilmNegative() : 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}), 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")))) + 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")))) { spotgrid->get_style_context()->add_class("grid-spacing"); setExpandAlignProperties(spotgrid, true, false, Gtk::ALIGN_FILL, Gtk::ALIGN_CENTER); @@ -64,7 +80,10 @@ FilmNegative::FilmNegative() : 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"))); + spotbutton->set_image(*Gtk::manage(new RTImage("color-picker-small.png"))); + + filmBaseSpotButton->set_tooltip_text(M("TP_FILMNEGATIVE_FILMBASE_TOOLTIP")); + setExpandAlignProperties(filmBaseValuesLabel, false, false, Gtk::ALIGN_START, Gtk::ALIGN_CENTER); // TODO make spot size configurable ? @@ -81,7 +100,7 @@ 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); @@ -90,13 +109,26 @@ FilmNegative::FilmNegative() : pack_start(*blueRatio, Gtk::PACK_SHRINK, 0); pack_start(*spotgrid, 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); + + pack_start(*filmBaseSpotButton, 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)); + // Editing geometry; create the spot rectangle Rectangle* const spotRect = new Rectangle(); spotRect->filled = false; - + visibleGeometry.push_back(spotRect); // Stick a dummy rectangle over the whole image in mouseOverGeometry. @@ -134,6 +166,14 @@ void FilmNegative::read(const rtengine::procparams::ProcParams* pp, const Params 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; + + // If base values are not set in params, estimated values will be passed in later + // (after processing) via FilmNegListener + filmBaseValuesLabel->set_text(formatBaseValues(filmBaseValues)); + enableListener(); } @@ -142,14 +182,23 @@ void FilmNegative::write(rtengine::procparams::ProcParams* pp, ParamsEdited* ped pp->filmNegative.redRatio = redRatio->getValue(); pp->filmNegative.greenExp = greenExp->getValue(); pp->filmNegative.blueRatio = blueRatio->getValue(); + pp->filmNegative.enabled = getEnabled(); if (pedited) { 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.enabled = !get_inconsistent(); } + + pp->filmNegative.redBase = filmBaseValues[0]; + pp->filmNegative.greenBase = filmBaseValues[1]; + pp->filmNegative.blueBase = filmBaseValues[2]; + } void FilmNegative::setDefaults(const rtengine::procparams::ProcParams* defParams, const ParamsEdited* pedited) @@ -172,8 +221,8 @@ void FilmNegative::setDefaults(const rtengine::procparams::ProcParams* defParams void FilmNegative::setBatchMode(bool batchMode) { if (batchMode) { - spotConn.disconnect(); removeIfThere(this, spotgrid, false); + removeIfThere(this, filmBaseSpotButton, false); ToolPanel::setBatchMode(batchMode); redRatio->showEditedCB(); greenExp->showEditedCB(); @@ -205,16 +254,20 @@ void FilmNegative::enabledChanged() if (listener) { if (get_inconsistent()) { listener->panelChanged(evFilmNegativeEnabled, M("GENERAL_UNCHANGED")); - } - else if (getEnabled()) { + } else if (getEnabled()) { listener->panelChanged(evFilmNegativeEnabled, M("GENERAL_ENABLED")); - } - else { + } else { listener->panelChanged(evFilmNegativeEnabled, M("GENERAL_DISABLED")); } } } +void FilmNegative::filmBaseValuesChanged(std::array rgb) +{ + filmBaseValues = rgb; + filmBaseValuesLabel->set_text(formatBaseValues(filmBaseValues)); +} + void FilmNegative::setFilmNegProvider(FilmNegProvider* provider) { fnp = provider; @@ -227,7 +280,7 @@ void FilmNegative::setEditProvider(EditDataProvider* provider) CursorShape FilmNegative::getCursor(int objectID) const { - return CSSpotWB; + return CSSpotWB; } bool FilmNegative::mouseOver(int modifierKey) @@ -246,31 +299,57 @@ bool FilmNegative::button1Pressed(int modifierKey) EditSubscriber::action = EditSubscriber::Action::NONE; if (listener) { - refSpotCoords.push_back(provider->posImage); + if (spotbutton->get_active()) { - if (refSpotCoords.size() == 2) { - // User has selected 2 reference gray spots. Calculating new exponents - // from channel values and updating parameters. + refSpotCoords.push_back(provider->posImage); - std::array newExps; - if (fnp->getFilmNegativeExponents(refSpotCoords[0], refSpotCoords[1], newExps)) { + if (refSpotCoords.size() == 2) { + // User has selected 2 reference gray spots. Calculating new exponents + // from channel values and updating parameters. + + std::array newExps; + + if (fnp->getFilmNegativeExponents(refSpotCoords[0], refSpotCoords[1], newExps)) { + disableListener(); + // 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]); + enableListener(); + + if (listener && getEnabled()) { + listener->panelChanged( + evFilmNegativeExponents, + Glib::ustring::compose( + "Ref=%1\nR=%2\nB=%3", + greenExp->getValue(), + redRatio->getValue(), + blueRatio->getValue() + ) + ); + } + } + + switchOffEditMode(); + } + + } else if (filmBaseSpotButton->get_active()) { + + std::array newBaseLev; + + if (fnp->getRawSpotValues(provider->posImage, 32, newBaseLev)) { disableListener(); - // 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]); + + filmBaseValues = newBaseLev; + enableListener(); + const Glib::ustring vs = formatBaseValues(filmBaseValues); + + filmBaseValuesLabel->set_text(vs); + if (listener && getEnabled()) { - listener->panelChanged( - evFilmNegativeExponents, - Glib::ustring::compose( - "Ref=%1\nR=%2\nB=%3", - greenExp->getValue(), - redRatio->getValue(), - blueRatio->getValue() - ) - ); + listener->panelChanged(evFilmBaseValues, vs); } } @@ -292,11 +371,38 @@ void FilmNegative::switchOffEditMode() refSpotCoords.clear(); unsubscribe(); spotbutton->set_active(false); + filmBaseSpotButton->set_active(false); } void FilmNegative::editToggled() { if (spotbutton->get_active()) { + + filmBaseSpotButton->set_active(false); + refSpotCoords.clear(); + + subscribe(); + + int w, h; + getEditProvider()->getImageSize(w, h); + + // Stick a dummy rectangle over the whole image in mouseOverGeometry. + // 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(); + } +} + +void FilmNegative::baseSpotToggled() +{ + if (filmBaseSpotButton->get_active()) { + + spotbutton->set_active(false); + refSpotCoords.clear(); + subscribe(); int w, h; diff --git a/rtgui/filmnegative.h b/rtgui/filmnegative.h index bca155ceb..0810a8c57 100644 --- a/rtgui/filmnegative.h +++ b/rtgui/filmnegative.h @@ -33,13 +33,15 @@ 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; }; class FilmNegative final : public ToolParamBlock, public AdjusterListener, public FoldableToolPanel, - public EditSubscriber + public EditSubscriber, + public rtengine::FilmNegListener { public: FilmNegative(); @@ -53,6 +55,8 @@ public: void adjusterChanged(Adjuster* a, double newval) override; void enabledChanged() override; + void filmBaseValuesChanged(std::array rgb) override; + void setFilmNegProvider(FilmNegProvider* provider); void setEditProvider(EditDataProvider* provider) override; @@ -66,12 +70,16 @@ public: private: void editToggled(); + void baseSpotToggled(); const rtengine::ProcEvent evFilmNegativeExponents; const rtengine::ProcEvent evFilmNegativeEnabled; + const rtengine::ProcEvent evFilmBaseValues; std::vector refSpotCoords; + std::array filmBaseValues; + FilmNegProvider* fnp; Adjuster* const greenExp; @@ -80,5 +88,9 @@ private: Gtk::Grid* const spotgrid; Gtk::ToggleButton* const spotbutton; - sigc::connection spotConn; + + Gtk::Label* const filmBaseLabel; + Gtk::Label* const filmBaseValuesLabel; + Gtk::ToggleButton* const filmBaseSpotButton; + }; diff --git a/rtgui/paramsedited.cc b/rtgui/paramsedited.cc index 0faee0fd3..6a25a9d64 100644 --- a/rtgui/paramsedited.cc +++ b/rtgui/paramsedited.cc @@ -623,6 +623,7 @@ void ParamsEdited::set(bool v) filmNegative.redRatio = v; filmNegative.greenExp = v; filmNegative.blueRatio = v; + filmNegative.baseValues = v; exif = v; iptc = v; @@ -1225,6 +1226,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; // How the hell can we handle that??? // exif = exif && p.exif==other.exif @@ -3430,6 +3434,12 @@ 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; + } + // Exif changes are added to the existing ones if (exif) { for (procparams::ExifPairs::const_iterator i = mods.exif.begin(); i != mods.exif.end(); ++i) { @@ -3476,7 +3486,7 @@ bool RetinexParamsEdited::isUnchanged() const bool FilmNegativeParamsEdited::isUnchanged() const { - return enabled && redRatio && greenExp && blueRatio; + return enabled && redRatio && greenExp && blueRatio && baseValues; } bool CaptureSharpeningParamsEdited::isUnchanged() const diff --git a/rtgui/paramsedited.h b/rtgui/paramsedited.h index d1be2c57e..1feedd774 100644 --- a/rtgui/paramsedited.h +++ b/rtgui/paramsedited.h @@ -727,6 +727,7 @@ struct FilmNegativeParamsEdited { bool redRatio; bool greenExp; bool blueRatio; + bool baseValues; bool isUnchanged() const; }; diff --git a/rtgui/partialpastedlg.cc b/rtgui/partialpastedlg.cc index 22f608ae4..b31852af2 100644 --- a/rtgui/partialpastedlg.cc +++ b/rtgui/partialpastedlg.cc @@ -986,6 +986,7 @@ 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; } if (!captureSharpening->get_active ()) { diff --git a/rtgui/ppversion.h b/rtgui/ppversion.h index 89f93ed44..6a670733b 100644 --- a/rtgui/ppversion.h +++ b/rtgui/ppversion.h @@ -1,11 +1,13 @@ #pragma once // This number has to be incremented whenever the PP3 file format is modified or the behaviour of a tool changes -#define PPVERSION 346 +#define PPVERSION 347 #define PPVERSION_AEXP 301 //value of PPVERSION when auto exposure algorithm was modified /* Log of version changes + 347 2019-11-17 + added special values in filmNegative for backwards compatibility with previous channel scaling method 346 2019-01-01 changed microcontrast uniformity 345 2018-10-21 diff --git a/rtgui/toolpanelcoord.cc b/rtgui/toolpanelcoord.cc index 00ad96328..a9c3231d4 100644 --- a/rtgui/toolpanelcoord.cc +++ b/rtgui/toolpanelcoord.cc @@ -609,6 +609,7 @@ void ToolPanelCoordinator::initImage (rtengine::StagedImageProcessor* ipc_, bool ipc->setSizeListener (crop); ipc->setSizeListener (resize); ipc->setImageTypeListener (this); + ipc->setFilmNegListener (filmNegative); flatfield->setShortcutPath (Glib::path_get_dirname (ipc->getInitialImage()->getFileName())); icm->setRawMeta (raw, (const rtengine::FramesData*)pMetaData); @@ -1073,3 +1074,8 @@ bool ToolPanelCoordinator::getFilmNegativeExponents(rtengine::Coord spotA, rteng { 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); +} \ No newline at end of file diff --git a/rtgui/toolpanelcoord.h b/rtgui/toolpanelcoord.h index 0fc1a9070..1b51bca31 100644 --- a/rtgui/toolpanelcoord.h +++ b/rtgui/toolpanelcoord.h @@ -297,6 +297,7 @@ public: // 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; // rotatelistener interface void straightenRequested () override;