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 }