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