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;