File indexing completed on 2024-12-22 04:16:03
0001 /* 0002 * SPDX-FileCopyrightText: 2021 Dmitry Kazakov <dimula73@gmail.com> 0003 * 0004 * SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 0007 #ifndef KRITA_KISCOLORSMUDGESAMPLEUTILS_H 0008 #define KRITA_KISCOLORSMUDGESAMPLEUTILS_H 0009 0010 #include "kis_fixed_paint_device.h" 0011 #include "KoMixColorsOp.h" 0012 #include "kis_algebra_2d.h" 0013 0014 #include <KoColor.h> 0015 0016 #include "kis_fixed_paint_device.h" 0017 #include "KisColorSmudgeSource.h" 0018 0019 0020 namespace KisColorSmudgeSampleUtils { 0021 0022 struct WeightedSampleWrapper 0023 { 0024 WeightedSampleWrapper(KoMixColorsOp::Mixer *mixer, 0025 KisFixedPaintDeviceSP maskDab, const QRect &maskRect, 0026 KisFixedPaintDeviceSP sampleDab, const QRect &sampleRect) 0027 : m_mixer(mixer), 0028 m_maskRect(maskRect), 0029 m_maskPtr(maskDab->data()), 0030 m_maskStride(maskDab->bounds().width()), 0031 m_samplePixelSize(sampleDab->colorSpace()->pixelSize()), 0032 m_sampleRect(sampleRect), 0033 m_samplePtr(sampleDab->data()), 0034 m_sampleStride(sampleDab->bounds().width() * m_samplePixelSize) 0035 { 0036 0037 } 0038 0039 inline void samplePixel(const QPoint &relativeSamplePoint) { 0040 const QPoint maskPt(relativeSamplePoint - m_maskRect.topLeft() + m_sampleRect.topLeft()); 0041 0042 const qint16 opacity = *(m_maskPtr + maskPt.x() + maskPt.y() * m_maskStride); 0043 const quint8 *ptr = m_samplePtr + relativeSamplePoint.x() * m_samplePixelSize + relativeSamplePoint.y() * m_sampleStride; 0044 0045 m_mixer->accumulate(ptr, &opacity, opacity, 1); 0046 } 0047 0048 static void verifySampleRadiusValue(qreal *sampleRadiusValue) { 0049 KIS_SAFE_ASSERT_RECOVER(*sampleRadiusValue <= 1.0) { 0050 *sampleRadiusValue = 1.0; 0051 } 0052 } 0053 0054 0055 bool shouldRestartWithBiggerRadius() const { 0056 // if all the pixels we sampled appeared to be masked out 0057 // we should ask the sampling algorithm to restart with 0058 // bigger sampling radius 0059 0060 return m_mixer->currentWeightsSum() < 128; 0061 } 0062 0063 KoMixColorsOp::Mixer *m_mixer; 0064 const QRect m_maskRect; 0065 quint8 *m_maskPtr; 0066 const int m_maskStride; 0067 int m_samplePixelSize; 0068 const QRect m_sampleRect; 0069 quint8 *m_samplePtr; 0070 const int m_sampleStride; 0071 }; 0072 0073 struct AveragedSampleWrapper 0074 { 0075 AveragedSampleWrapper(KoMixColorsOp::Mixer *mixer, 0076 KisFixedPaintDeviceSP maskDab, const QRect &maskRect, 0077 KisFixedPaintDeviceSP sampleDab, const QRect &sampleRect) 0078 : m_mixer(mixer), 0079 m_samplePixelSize(sampleDab->colorSpace()->pixelSize()), 0080 m_sampleRect(sampleRect), 0081 m_samplePtr(sampleDab->data()), 0082 m_sampleStride(sampleDab->bounds().width() * m_samplePixelSize) 0083 { 0084 Q_UNUSED(maskDab); 0085 Q_UNUSED(maskRect); 0086 } 0087 0088 inline void samplePixel(const QPoint &relativeSamplePoint) { 0089 const quint8 *ptr = m_samplePtr + relativeSamplePoint.x() * m_samplePixelSize + relativeSamplePoint.y() * m_sampleStride; 0090 m_mixer->accumulateAverage(ptr, 1); 0091 } 0092 0093 static void verifySampleRadiusValue(qreal *sampleRadiusValue) { 0094 Q_UNUSED(sampleRadiusValue); 0095 } 0096 0097 bool shouldRestartWithBiggerRadius() const { 0098 return false; 0099 } 0100 0101 KoMixColorsOp::Mixer *m_mixer; 0102 int m_samplePixelSize; 0103 const QRect m_sampleRect; 0104 quint8 *m_samplePtr; 0105 const int m_sampleStride; 0106 }; 0107 0108 /** 0109 * Sample color from \p srcRect in weighted way 0110 * 0111 * The function samples \p srcRect in an efficient way. It samples "random" 0112 * pixels using Halton sequence and waits until the sampled color "converges" 0113 * to stable value. The sampled area is defined by \p sampleRadiusValue. 0114 * 0115 * The way of weighting the pixels is defined by \p WeightingModeWrapper 0116 * template policy. If `WeightedSampleWrapper` is used, then the sampled color 0117 * is weighted by the passed brush mask in \p maskDab. If 0118 * `AveragedSampleWrapper` is used, then \p maskDab is **not** used and all 0119 * sampled pixels are averaged in a uniform way. 0120 * 0121 * \param sampleRadiusValue defines how many pixels are sampled. When 0122 * \p sampleRadiusValue is 0.0, only the central pixel is sampled. When 0123 * \p sampleRadiusValue is 1.0, the entire range of \p srcRect is sampled. 0124 * If `AveragedSampleWrapper` is used, then \p sampleRadiusValue may be 0125 * increased up to 3.0 to sample outside \p srcRect. 0126 * 0127 * When `WeightedSampleWrapper` is used, the sampler may sample more pixels 0128 * than actually requested by \p sampleRadiusValue. It may happen if all the 0129 * pixels in the sample radius area are masked out by \p maskDab. 0130 * 0131 * \param tempFixedDevice is a temporary device that may be used by the 0132 * function for internal purposes. 0133 */ 0134 template<class WeightingModeWrapper> 0135 void sampleColor(const QRect &srcRect, 0136 qreal sampleRadiusValue, 0137 KisColorSmudgeSourceSP sourceDevice, 0138 KisFixedPaintDeviceSP tempFixedDevice, 0139 KisFixedPaintDeviceSP maskDab, 0140 KoColor *resultColor) 0141 0142 { 0143 WeightingModeWrapper::verifySampleRadiusValue(&sampleRadiusValue); 0144 0145 KIS_ASSERT_RECOVER_RETURN(*resultColor->colorSpace() == *sourceDevice->colorSpace()); 0146 KIS_ASSERT_RECOVER_RETURN(*tempFixedDevice->colorSpace() == *sourceDevice->colorSpace()); 0147 0148 const QRect minimalRect = QRect(srcRect.center(), QSize(1,1)); 0149 0150 do { 0151 const QRect sampleRect = sampleRadiusValue > 0 ? 0152 KisAlgebra2D::blowRect(srcRect, 0.5 * (sampleRadiusValue - 1.0)) | minimalRect : 0153 minimalRect; 0154 0155 tempFixedDevice->setRect(sampleRect); 0156 tempFixedDevice->lazyGrowBufferWithoutInitialization(); 0157 0158 const KoColorSpace *cs = tempFixedDevice->colorSpace(); 0159 const int numPixels = sampleRect.width() * sampleRect.height(); 0160 sourceDevice->readRect(sampleRect); 0161 sourceDevice->readBytes(tempFixedDevice->data(), sampleRect); 0162 0163 KisAlgebra2D::HaltonSequenceGenerator hGen(2); 0164 KisAlgebra2D::HaltonSequenceGenerator vGen(3); 0165 0166 QScopedPointer<KoMixColorsOp::Mixer> mixer(cs->mixColorsOp()->createMixer()); 0167 0168 const int minSamples = 0169 qMin(numPixels, qMax(64, qRound(0.02 * numPixels))); 0170 0171 WeightingModeWrapper weightingModeWrapper(mixer.data(), 0172 maskDab, srcRect, 0173 tempFixedDevice, sampleRect); 0174 0175 KoColor lastPickedColor(*resultColor); 0176 0177 for (int i = 0; i < minSamples; i++) { 0178 const QPoint pt(hGen.generate(sampleRect.width() - 1), 0179 vGen.generate(sampleRect.height() - 1)); 0180 0181 weightingModeWrapper.samplePixel(pt); 0182 } 0183 0184 mixer->computeMixedColor(resultColor->data()); 0185 lastPickedColor = *resultColor; 0186 0187 const int batchSize = 16; 0188 int numSamplesLeft = numPixels - minSamples; 0189 0190 while (numSamplesLeft > 0) { 0191 const int currentBatchSize = qMin(numSamplesLeft, batchSize); 0192 for (int i = 0; i < currentBatchSize; i++) { 0193 const QPoint pt(hGen.generate(sampleRect.width() - 1), 0194 vGen.generate(sampleRect.height() - 1)); 0195 0196 weightingModeWrapper.samplePixel(pt); 0197 } 0198 0199 mixer->computeMixedColor(resultColor->data()); 0200 0201 const quint8 difference = 0202 cs->differenceA(resultColor->data(), lastPickedColor.data()); 0203 0204 if (difference <= 2) break; 0205 0206 lastPickedColor = *resultColor; 0207 numSamplesLeft -= currentBatchSize; 0208 } 0209 0210 if (!weightingModeWrapper.shouldRestartWithBiggerRadius() || sampleRadiusValue >= 1.0) { 0211 break; 0212 } 0213 0214 sampleRadiusValue = qMin(1.0, sampleRadiusValue + 0.05); 0215 0216 } while (1); 0217 } 0218 0219 } 0220 0221 0222 #endif //KRITA_KISCOLORSMUDGESAMPLEUTILS_H