File indexing completed on 2024-05-12 15:58:49

0001 /*
0002  * This file is part of Krita
0003  *
0004  * SPDX-FileCopyrightText: 2021 Deif Lou <ginoba@gmail.com>
0005  *
0006  * SPDX-License-Identifier: GPL-2.0-or-later
0007  */
0008 
0009 #include <cmath>
0010 
0011 #include <kis_histogram.h>
0012 
0013 #include "KisAutoLevels.h"
0014 
0015 namespace KisAutoLevels
0016 {
0017 
0018 QPair<qreal, qreal> getMeanAndMedian(ChannelHistogram histogram, qreal begin, qreal end)
0019 {
0020     Q_ASSERT(histogram.histogram);
0021     Q_ASSERT(histogram.channel >= 0 && histogram.channel < histogram.histogram->producer()->channels().size());
0022 
0023     histogram.histogram->setChannel(histogram.channel);
0024 
0025     const int numberOfBins = histogram.histogram->producer()->numberOfBins();
0026     const int beginBin = static_cast<int>(begin * static_cast<qreal>(numberOfBins));
0027     const int endBin = static_cast<int>(end * static_cast<qreal>(numberOfBins)) + 1;
0028 
0029     qreal numberOfSamples = 0.0;
0030     qreal meanSum = 0.0;
0031 
0032     for (int i = beginBin; i < endBin; ++i) {
0033         const qreal current = static_cast<qreal>(histogram.histogram->getValue(i));
0034         numberOfSamples += current;
0035         meanSum += current * static_cast<qreal>(i);
0036     }
0037 
0038     const qreal mean = meanSum / (numberOfSamples * static_cast<qreal>(numberOfBins));
0039     qreal probabilityAccumulator = 0.0;
0040     qreal median = 0.0;
0041 
0042     for (int i = beginBin; i < endBin; ++i) {
0043         const qreal probability = static_cast<qreal>(histogram.histogram->getValue(i)) / numberOfSamples;
0044         probabilityAccumulator += probability;
0045         if (probabilityAccumulator >= 0.5) {
0046             median = static_cast<qreal>(i) / static_cast<qreal>(numberOfBins);
0047             break;
0048         }
0049     }
0050 
0051     return {mean, median};
0052 }
0053 
0054 QPair<qreal, qreal> getInputBlackAndWhitePoints(ChannelHistogram histogram,
0055                                                 qreal shadowsClipping,
0056                                                 qreal highlightsClipping)
0057 {
0058     Q_ASSERT(histogram.histogram);
0059     Q_ASSERT(histogram.channel >= 0 && histogram.channel < histogram.histogram->producer()->channels().size());
0060 
0061     histogram.histogram->setChannel(histogram.channel);
0062 
0063     int numberOfBins = histogram.histogram->producer()->numberOfBins();
0064 
0065     Q_ASSERT(numberOfBins > 1);
0066 
0067     const qreal totalNumberOfSamples = static_cast<qreal>(histogram.histogram->producer()->count());
0068 
0069     // This basically integrates the probability mass function given by the
0070     // histogram, from the left and the right, until the thresholds given by the
0071     // clipping are reached, to obtain the black and white points
0072 
0073     int blackPoint = 0;
0074     qreal accumulator = 0.0;
0075     for (int i = 0; i < numberOfBins; ++i) {
0076         const qreal sampleCountForBin = static_cast<qreal>(histogram.histogram->getValue(i));
0077         const qreal probability = sampleCountForBin / totalNumberOfSamples;
0078 
0079         accumulator += probability;
0080         if (accumulator > shadowsClipping) {
0081             break;
0082         }
0083         blackPoint = i;
0084     }
0085 
0086     int whitePoint = numberOfBins - 1;
0087     accumulator = 0.0;
0088     for (int i = numberOfBins - 1; i >= 0; --i) {
0089         const qreal sampleCountForBin = static_cast<qreal>(histogram.histogram->getValue(i));
0090         const qreal probability = sampleCountForBin / totalNumberOfSamples;
0091 
0092         accumulator += probability;
0093         if (accumulator > highlightsClipping) {
0094             break;
0095         }
0096         whitePoint = i;
0097     }
0098 
0099     if (whitePoint <= blackPoint) {
0100         if (blackPoint + 1 == numberOfBins) {
0101             whitePoint = blackPoint;
0102             --blackPoint;
0103         } else {
0104             whitePoint = blackPoint + 1;
0105         }
0106     }
0107 
0108     return
0109         {
0110             static_cast<qreal>(blackPoint) / static_cast<qreal>(numberOfBins),
0111             static_cast<qreal>(whitePoint) / static_cast<qreal>(numberOfBins)
0112         };
0113 }
0114 
0115 QPair<KoColor, KoColor> getDarkestAndWhitestColors(const KisPaintDeviceSP device,
0116                                                    qreal shadowsClipping,
0117                                                    qreal highlightsClipping);
0118 
0119 qreal getGamma(qreal blackPoint, qreal whitePoint, qreal inputIntensity, qreal outputIntensity)
0120 {
0121     Q_ASSERT(blackPoint < whitePoint);
0122     Q_ASSERT(inputIntensity >= blackPoint && inputIntensity <= whitePoint);
0123     
0124     if (qFuzzyIsNull(outputIntensity)) {
0125         return 0.01;
0126     }
0127     if (qFuzzyCompare(outputIntensity, 1.0)) {
0128         return 10.0;
0129     }
0130 
0131     const qreal inputIntensityAfterLinearMapping =
0132         (inputIntensity - blackPoint) / (whitePoint - blackPoint);
0133 
0134     return qBound(0.01, log(inputIntensityAfterLinearMapping) / log(outputIntensity), 10.0);
0135 }
0136 
0137 
0138 QVector<KisLevelsCurve> adjustMonochromaticContrast(ChannelHistogram lightnessHistogram,
0139                                                    QVector<ChannelHistogram> &channelsHistograms,
0140                                                    qreal shadowsClipping,
0141                                                    qreal highlightsClipping,
0142                                                    qreal maximumInputBlackAndWhiteOffset,
0143                                                    MidtonesAdjustmentMethod midtonesAdjustmentMethod,
0144                                                    qreal midtonesAdjustmentAmount,
0145                                                    const QVector<qreal> &outputBlackPoints,
0146                                                    const QVector<qreal> &outputWhitePoints,
0147                                                    const QVector<qreal> &outputMidtones)
0148 {
0149     Q_ASSERT(lightnessHistogram.histogram);
0150     Q_ASSERT(lightnessHistogram.channel >= 0);
0151     Q_ASSERT(lightnessHistogram.channel < lightnessHistogram.histogram->producer()->channels().size());
0152     Q_ASSERT(outputBlackPoints.size() == channelsHistograms.size());
0153     Q_ASSERT(outputWhitePoints.size() == channelsHistograms.size());
0154     Q_ASSERT(outputMidtones.size() == channelsHistograms.size());
0155 
0156     QVector<KisLevelsCurve> levelsCurves;
0157 
0158     const QPair<qreal, qreal> inputBlackAndWhitePoints =
0159         getInputBlackAndWhitePoints(lightnessHistogram, shadowsClipping, highlightsClipping);
0160     const qreal inputBlackPoint = qMin(maximumInputBlackAndWhiteOffset, inputBlackAndWhitePoints.first);
0161     const qreal inputWhitePoint = qMax(1.0 - maximumInputBlackAndWhiteOffset, inputBlackAndWhitePoints.second);
0162     const qreal linearMappingMidPoint = (inputBlackPoint + inputWhitePoint) / 2.0;
0163 
0164     for (int i = 0; i < channelsHistograms.size(); ++i) {
0165         ChannelHistogram &channelHistogram = channelsHistograms[i];
0166             
0167         qreal gamma = 1.0;
0168         if (midtonesAdjustmentMethod != MidtonesAdjustmentMethod_None &&
0169             channelHistogram.histogram &&
0170             channelHistogram.channel >= 0 &&
0171             channelHistogram.channel < channelHistogram.histogram->producer()->channels().size()) {
0172 
0173             qreal inputIntensity;
0174             QPair<qreal, qreal> meanAndMedian = getMeanAndMedian(channelHistogram, inputBlackPoint, inputWhitePoint);
0175 
0176             if (midtonesAdjustmentMethod == MidtonesAdjustmentMethod_UseMean) {
0177                 inputIntensity = meanAndMedian.first;
0178             } else {
0179                 inputIntensity = meanAndMedian.second;
0180             }
0181 
0182             inputIntensity = linearMappingMidPoint + (inputIntensity - linearMappingMidPoint) * midtonesAdjustmentAmount;
0183             gamma = getGamma(inputBlackPoint, inputWhitePoint, inputIntensity, outputMidtones[i]);
0184         }
0185 
0186         levelsCurves.append(
0187             KisLevelsCurve(
0188                 inputBlackPoint,
0189                 inputWhitePoint,
0190                 gamma,
0191                 outputBlackPoints[i],
0192                 outputWhitePoints[i]
0193             )
0194         );
0195     }
0196 
0197     return levelsCurves;
0198 }
0199 
0200 QVector<KisLevelsCurve> adjustPerChannelContrast(QVector<ChannelHistogram> &channelsHistograms,
0201                                                 qreal shadowsClipping,
0202                                                 qreal highlightsClipping,
0203                                                 qreal maximumInputBlackAndWhiteOffset,
0204                                                 MidtonesAdjustmentMethod midtonesAdjustmentMethod,
0205                                                 qreal midtonesAdjustmentAmount,
0206                                                 const QVector<qreal> &outputBlackPoints,
0207                                                 const QVector<qreal> &outputWhitePoints,
0208                                                 const QVector<qreal> &outputMidtones)
0209 {
0210     QVector<KisLevelsCurve> levelsCurves;
0211 
0212     for (int i = 0; i < channelsHistograms.size(); ++i) {
0213         QVector<ChannelHistogram> channelHistogram{channelsHistograms[i]};
0214         levelsCurves.append(
0215             adjustMonochromaticContrast(
0216                 channelHistogram[0],
0217                 channelHistogram,
0218                 shadowsClipping,
0219                 highlightsClipping,
0220                 maximumInputBlackAndWhiteOffset,
0221                 midtonesAdjustmentMethod,
0222                 midtonesAdjustmentAmount,
0223                 {outputBlackPoints[i]},
0224                 {outputWhitePoints[i]},
0225                 {outputMidtones[i]}
0226             )
0227         );
0228     }
0229 
0230     return levelsCurves;
0231 }
0232 
0233 }