File indexing completed on 2024-12-22 04:10:14

0001 /*
0002  *  SPDX-FileCopyrightText: 2007 Boudewijn Rempt boud @valdyas.org
0003  *
0004  *  SPDX-License-Identifier: GPL-2.0-or-later
0005  */
0006 
0007 #include "kis_convolution_painter_test.h"
0008 
0009 #include <simpletest.h>
0010 
0011 #include <QBitArray>
0012 #include <QElapsedTimer>
0013 
0014 #include <KoColor.h>
0015 #include <KoColorSpace.h>
0016 #include <KoColorSpaceRegistry.h>
0017 #include <KoColorSpaceTraits.h>
0018 
0019 #include "kis_paint_device.h"
0020 #include "kis_convolution_painter.h"
0021 #include "kis_convolution_kernel.h"
0022 #include <kis_gaussian_kernel.h>
0023 #include <kis_mask_generator.h>
0024 #include <kistest.h>
0025 #include "testutil.h"
0026 #include "testing_timed_default_bounds.h"
0027 
0028 KisPaintDeviceSP initAsymTestDevice(QRect &imageRect, int &pixelSize, QByteArray &initialData)
0029 {
0030     KisPaintDeviceSP dev = new KisPaintDevice(KoColorSpaceRegistry::instance()->rgb8());
0031     pixelSize = dev->pixelSize();
0032 
0033     imageRect = QRect(0,0,5,5);
0034     initialData.resize(25 * pixelSize);
0035 
0036     quint8 *ptr = (quint8*) initialData.data();
0037     for(int i = 0; i < 25; i++) {
0038         KoColor pixel(QColor(i,i,i,255), dev->colorSpace());
0039         memcpy(ptr, pixel.data(), pixelSize);
0040 
0041         ptr += pixelSize;
0042     }
0043 
0044     dev->writeBytes((const quint8*)initialData.constData(), imageRect);
0045 
0046     return dev;
0047 }
0048 
0049 Eigen::Matrix<qreal, 3, 3> initSymmFilter(qreal &offset, qreal &factor)
0050 {
0051     Eigen::Matrix<qreal, 3, 3> filter;
0052     filter(0,0) = 1.0 / 21;
0053     filter(0,1) = 3.0 / 21;
0054     filter(0,2) = 1.0 / 21;
0055 
0056     filter(1,0) = 3.0 / 21;
0057     filter(1,1) = 5.0 / 21;
0058     filter(1,2) = 3.0 / 21;
0059 
0060     filter(2,0) = 1.0 / 21;
0061     filter(2,1) = 3.0 / 21;
0062     filter(2,2) = 1.0 / 21;
0063 
0064     offset = 0.0;
0065     factor = 1.0;
0066 
0067     return filter;
0068 }
0069 
0070 Eigen::Matrix<qreal, 3, 3> initAsymmFilter(qreal &offset, qreal &factor)
0071 {
0072     Eigen::Matrix<qreal, 3, 3> filter;
0073     filter(0,0) = 1.0;
0074     filter(1,0) = 2.0;
0075     filter(2,0) = 1.0;
0076 
0077     filter(0,1) = 0.0;
0078     filter(1,1) = 1.0;
0079     filter(2,1) = 0.0;
0080 
0081     filter(0,2) =-1.0;
0082     filter(1,2) =-2.0;
0083     filter(2,2) =-1.0;
0084 
0085     offset = 0.0;
0086     factor = 1.0;
0087 
0088     return filter;
0089 }
0090 
0091 void printPixel(QString prefix, int pixelSize, quint8 *data) {
0092     QString str = prefix;
0093 
0094     for(int i = 0; i < pixelSize; i++) {
0095         str += ' ';
0096         str += QString::number(data[i]);
0097     }
0098 
0099     dbgKrita << str;
0100 }
0101 
0102 void KisConvolutionPainterTest::testIdentityConvolution()
0103 {
0104     QImage qimage(QString(FILES_DATA_DIR) + '/' + "hakonepa.png");
0105 
0106     KisDefaultBoundsBaseSP bounds = new TestUtil::TestingTimedDefaultBounds(qimage.rect());
0107 
0108     KisPaintDeviceSP dev = new KisPaintDevice(KoColorSpaceRegistry::instance()->rgb8());
0109     dev->setDefaultBounds(bounds);
0110     dev->convertFromQImage(qimage, 0, 0, 0);
0111 
0112     KisConvolutionKernelSP kernel = new KisConvolutionKernel(3, 3, 0, 0);
0113     kernel->data()(0) = 0;
0114     kernel->data()(1) = 0;
0115     kernel->data()(2) = 0;
0116     kernel->data()(3) = 0;
0117     kernel->data()(4) = 1;
0118     kernel->data()(5) = 0;
0119     kernel->data()(6) = 0;
0120     kernel->data()(7) = 0;
0121     kernel->data()(8) = 0;
0122     KisConvolutionPainter gc(dev);
0123     gc.beginTransaction();
0124     gc.applyMatrix(kernel, dev, QPoint(0, 0), QPoint(0, 0), QSize(qimage.width(), qimage.height()));
0125     gc.deleteTransaction();
0126 
0127     QImage resultImage = dev->convertToQImage(0, 0, 0, qimage.width(), qimage.height());
0128 
0129     QPoint errpoint;
0130     if (!TestUtil::compareQImages(errpoint, qimage, resultImage)) {
0131         resultImage.save("identity_convolution.png");
0132         QFAIL(QString("Identity kernel did change image, first different pixel: %1,%2 ").arg(errpoint.x()).arg(errpoint.y()).toLatin1());
0133     }
0134 }
0135 
0136 void KisConvolutionPainterTest::testSymmConvolution()
0137 {
0138     qreal offset = 0.0;
0139     qreal factor = 1.0;
0140     Eigen::Matrix<qreal, 3, 3> filter = initSymmFilter(offset, factor);
0141 
0142     QRect imageRect;
0143     int pixelSize = 0;
0144     QByteArray initialData;
0145     KisPaintDeviceSP dev = initAsymTestDevice(imageRect, pixelSize, initialData);
0146 
0147     KisDefaultBoundsBaseSP bounds = new TestUtil::TestingTimedDefaultBounds(dev->exactBounds());
0148     dev->setDefaultBounds(bounds);
0149 
0150     KisConvolutionKernelSP kernel =
0151         KisConvolutionKernel::fromMatrix(filter, offset, factor);
0152     KisConvolutionPainter gc(dev);
0153     gc.beginTransaction();
0154 
0155     QRect filterRect = imageRect.adjusted(1,1,-1,-1);
0156     gc.applyMatrix(kernel, dev, filterRect.topLeft(), filterRect.topLeft(),
0157                    filterRect.size());
0158     gc.deleteTransaction();
0159 
0160     QByteArray resultData(initialData.size(), 0);
0161     dev->readBytes((quint8*)resultData.data(), imageRect);
0162 
0163     QCOMPARE(resultData, initialData);
0164 }
0165 
0166 void KisConvolutionPainterTest::testAsymmConvolutionImp(QBitArray channelFlags)
0167 {
0168     qreal offset = 0.0;
0169     qreal factor = 1.0;
0170     Eigen::Matrix<qreal, 3, 3> filter = initAsymmFilter(offset, factor);
0171 
0172     QRect imageRect;
0173     int pixelSize = -1;
0174     QByteArray initialData;
0175     KisPaintDeviceSP dev = initAsymTestDevice(imageRect, pixelSize, initialData);
0176 
0177     KisDefaultBoundsBaseSP bounds = new TestUtil::TestingTimedDefaultBounds(dev->exactBounds());
0178     dev->setDefaultBounds(bounds);
0179 
0180     KisConvolutionKernelSP kernel =
0181         KisConvolutionKernel::fromMatrix(filter, offset, factor);
0182     KisConvolutionPainter gc(dev);
0183     gc.beginTransaction();
0184     gc.setChannelFlags(channelFlags);
0185 
0186     QRect filterRect = imageRect.adjusted(1,1,-1,-1);
0187     gc.applyMatrix(kernel, dev, filterRect.topLeft(), filterRect.topLeft(),
0188                    filterRect.size());
0189     gc.deleteTransaction();
0190 
0191 
0192     QByteArray resultData(initialData.size(), 0);
0193     dev->readBytes((quint8*)resultData.data(), imageRect);
0194 
0195     QRect filteredRect = imageRect.adjusted(1, 1, -1, -1);
0196 
0197     quint8 *srcPtr = (quint8*) initialData.data();
0198     quint8 *resPtr = (quint8*) resultData.data();
0199 
0200     for(int row = 0; row < imageRect.height(); row++) {
0201         for(int col = 0; col < imageRect.width(); col++) {
0202 
0203             bool isFiltered = filteredRect.contains(col, row);
0204 
0205             int pixelValue = 8 + row * imageRect.width() + col;
0206             KoColor filteredPixel(QColor(pixelValue, pixelValue, pixelValue, 255), dev->colorSpace());
0207 
0208             KoColor resultPixel(dev->colorSpace());
0209             for(int j = 0; j < pixelSize; j++) {
0210                 resultPixel.data()[j] = isFiltered && channelFlags[j] ?
0211                     filteredPixel.data()[j] : srcPtr[j];
0212             }
0213 
0214             if(memcmp(resPtr, resultPixel.data(), pixelSize)) {
0215                 printPixel("Actual:  ", pixelSize, resPtr);
0216                 printPixel("Expected:", pixelSize, resultPixel.data());
0217                 QFAIL("Failed to filter area");
0218             }
0219 
0220             srcPtr += pixelSize;
0221             resPtr += pixelSize;
0222         }
0223     }
0224 }
0225 
0226 void KisConvolutionPainterTest::testAsymmAllChannels()
0227 {
0228     QBitArray channelFlags =
0229         KoColorSpaceRegistry::instance()->rgb8()->channelFlags(true, true);
0230     testAsymmConvolutionImp(channelFlags);
0231 }
0232 
0233 void KisConvolutionPainterTest::testAsymmSkipRed()
0234 {
0235     QBitArray channelFlags =
0236         KoColorSpaceRegistry::instance()->rgb8()->channelFlags(true, true);
0237     channelFlags[2] = false;
0238 
0239     testAsymmConvolutionImp(channelFlags);
0240 }
0241 
0242 void KisConvolutionPainterTest::testAsymmSkipGreen()
0243 {
0244     QBitArray channelFlags =
0245         KoColorSpaceRegistry::instance()->rgb8()->channelFlags(true, true);
0246     channelFlags[1] = false;
0247 
0248     testAsymmConvolutionImp(channelFlags);
0249 }
0250 
0251 void KisConvolutionPainterTest::testAsymmSkipBlue()
0252 {
0253     QBitArray channelFlags =
0254         KoColorSpaceRegistry::instance()->rgb8()->channelFlags(true, true);
0255     channelFlags[0] = false;
0256 
0257     testAsymmConvolutionImp(channelFlags);
0258 }
0259 
0260 void KisConvolutionPainterTest::testAsymmSkipAlpha()
0261 {
0262     QBitArray channelFlags =
0263         KoColorSpaceRegistry::instance()->rgb8()->channelFlags(true, true);
0264     channelFlags[3] = false;
0265 
0266     testAsymmConvolutionImp(channelFlags);
0267 }
0268 
0269 
0270 // #include <valgrind/callgrind.h>
0271 void KisConvolutionPainterTest::benchmarkConvolution()
0272 {
0273     QImage referenceImage(QString(FILES_DATA_DIR) + '/' + "hakonepa.png");
0274     QRect imageRect(QPoint(), referenceImage.size());
0275 
0276     KisPaintDeviceSP dev = new KisPaintDevice(KoColorSpaceRegistry::instance()->rgb8());
0277     dev->convertFromQImage(referenceImage, 0, 0, 0);
0278 
0279     KisDefaultBoundsBaseSP bounds = new TestUtil::TestingTimedDefaultBounds(referenceImage.rect());
0280     dev->setDefaultBounds(bounds);
0281 
0282 
0283     int diameter = 1;
0284 
0285     for (int i = 0; i < 4; i++) {
0286 
0287         KisCircleMaskGenerator* kas = new KisCircleMaskGenerator(diameter, 1.0, 5, 5, 2, false);
0288         KisConvolutionKernelSP kernel = KisConvolutionKernel::fromMaskGenerator(kas);
0289 
0290         KisConvolutionPainter gc(dev);
0291 
0292         QElapsedTimer timer; timer.start();
0293 
0294         // CALLGRIND_START_INSTRUMENTATION;
0295 
0296         gc.beginTransaction();
0297         gc.applyMatrix(kernel, dev, imageRect.topLeft(), imageRect.topLeft(),
0298                        imageRect.size());
0299         gc.deleteTransaction();
0300 
0301         // CALLGRIND_STOP_INSTRUMENTATION;
0302 
0303         dbgKrita << "Diameter:" << diameter << "time:" << timer.elapsed();
0304 
0305         if(diameter < 4) {
0306             diameter += 2;
0307         } else {
0308             diameter += 8;
0309         }
0310     }
0311 }
0312 
0313 void KisConvolutionPainterTest::testGaussianBase(KisPaintDeviceSP dev, bool useFftw, const QString &prefix)
0314 {
0315    QBitArray channelFlags =
0316        KoColorSpaceRegistry::instance()->rgb8()->channelFlags(true, true);
0317 
0318    KisPainter gc(dev);
0319 
0320 
0321    qreal horizontalRadius = 5, verticalRadius = 5;
0322 
0323    for(int i = 0; i < 3 ; i++, horizontalRadius+=5, verticalRadius+=5)
0324    {
0325        QElapsedTimer timer;
0326        timer.start();
0327 
0328        gc.beginTransaction();
0329 
0330        if (( horizontalRadius > 0 ) && ( verticalRadius > 0 )) {
0331            KisPaintDeviceSP interm = new KisPaintDevice(dev->colorSpace());
0332            interm->setDefaultBounds(dev->defaultBounds());
0333 
0334            KisConvolutionKernelSP kernelHoriz = KisGaussianKernel::createHorizontalKernel(horizontalRadius);
0335            KisConvolutionKernelSP kernelVertical = KisGaussianKernel::createVerticalKernel(verticalRadius);
0336 
0337            const QRect applyRect = dev->exactBounds();
0338 
0339            KisConvolutionPainter::EnginePreference enginePreference =
0340                useFftw ?
0341                KisConvolutionPainter::FFTW :
0342                KisConvolutionPainter::SPATIAL;
0343 
0344            KisConvolutionPainter horizPainter(interm, enginePreference);
0345            horizPainter.setChannelFlags(channelFlags);
0346            horizPainter.applyMatrix(kernelHoriz, dev,
0347                                     applyRect.topLeft() - QPoint(0, verticalRadius),
0348                                     applyRect.topLeft() - QPoint(0, verticalRadius),
0349                                     applyRect.size() + QSize(0, 2 * verticalRadius),
0350                                     BORDER_REPEAT);
0351 
0352            KisConvolutionPainter verticalPainter(dev, enginePreference);
0353            verticalPainter.setChannelFlags(channelFlags);
0354            verticalPainter.applyMatrix(kernelVertical, interm,
0355                                        applyRect.topLeft(),
0356                                        applyRect.topLeft(),
0357                                        applyRect.size(), BORDER_REPEAT);
0358 
0359            QImage result = dev->convertToQImage(0, applyRect.x(), applyRect.y(), applyRect.width(), applyRect.height());
0360 
0361            QString engine = useFftw ? "fftw" : "spatial";
0362            QString testCaseName = QString("test_gaussian_%1_%2_%3.png").arg(horizontalRadius).arg(verticalRadius).arg(engine);
0363 
0364            TestUtil::checkQImage(result,
0365                                  "convolution_painter_test",
0366                                  QString("gaussian_") + prefix,
0367                                  testCaseName);
0368 
0369            gc.revertTransaction();
0370        }
0371        dbgKrita << "Elapsed time:" << timer.elapsed() << "ms";
0372     }
0373 }
0374 
0375 
0376 void KisConvolutionPainterTest::testGaussian(bool useFftw)
0377 {
0378     QImage referenceImage(TestUtil::fetchDataFileLazy("kritaTransparent.png"));
0379     KisPaintDeviceSP dev = new KisPaintDevice(KoColorSpaceRegistry::instance()->rgb8());
0380     dev->convertFromQImage(referenceImage, 0, 0, 0);
0381 
0382     // TODO: fix the bounding rect to the rect of the reference image
0383     KisDefaultBoundsBaseSP bounds = new TestUtil::TestingTimedDefaultBounds(dev->exactBounds());
0384     dev->setDefaultBounds(bounds);
0385 
0386     testGaussianBase(dev, useFftw, "");
0387 }
0388 
0389 void KisConvolutionPainterTest::testGaussianSpatial()
0390 {
0391     testGaussian(false);
0392 }
0393 
0394 void KisConvolutionPainterTest::testGaussianFFTW()
0395 {
0396     testGaussian(true);
0397 }
0398 
0399 void KisConvolutionPainterTest::testGaussianSmall(bool useFftw)
0400 {
0401     KisPaintDeviceSP dev = new KisPaintDevice(KoColorSpaceRegistry::instance()->rgb8());
0402 
0403     KoColor c(Qt::yellow, dev->colorSpace());
0404 
0405     for (int i = 0; i < 50; i++) {
0406         quint8 baseOpacity = 75;
0407         KoColor c(Qt::magenta, dev->colorSpace());
0408 
0409         for (int j = 0; j <= 6; j++) {
0410             c.setOpacity(static_cast<quint8>(baseOpacity + 30 * j));
0411             dev->setPixel(i + j, i, c);
0412         }
0413     }
0414 
0415     KisDefaultBoundsBaseSP bounds = new TestUtil::TestingTimedDefaultBounds(dev->exactBounds());
0416     dev->setDefaultBounds(bounds);
0417 
0418     testGaussianBase(dev, useFftw, "reduced");
0419 }
0420 
0421 void KisConvolutionPainterTest::testGaussianSmallSpatial()
0422 {
0423     testGaussianSmall(false);
0424 }
0425 
0426 void KisConvolutionPainterTest::testGaussianSmallFFTW()
0427 {
0428     testGaussianSmall(true);
0429 }
0430 
0431 void KisConvolutionPainterTest::testGaussianDetails(bool useFftw)
0432 {
0433     QImage referenceImage(TestUtil::fetchDataFileLazy("resolution_test.png"));
0434     KisPaintDeviceSP dev = new KisPaintDevice(KoColorSpaceRegistry::instance()->rgb8());
0435     dev->convertFromQImage(referenceImage, 0, 0, 0);
0436 
0437     KisDefaultBoundsBaseSP bounds = new TestUtil::TestingTimedDefaultBounds(dev->exactBounds());
0438     dev->setDefaultBounds(bounds);
0439 
0440     testGaussianBase(dev, useFftw, "details");
0441 }
0442 
0443 void KisConvolutionPainterTest::testGaussianDetailsSpatial()
0444 {
0445     testGaussianDetails(false);
0446 }
0447 
0448 void KisConvolutionPainterTest::testGaussianDetailsFFTW()
0449 {
0450     testGaussianDetails(true);
0451 }
0452 
0453 #include "kis_transaction.h"
0454 
0455 void KisConvolutionPainterTest::testDilate()
0456 {
0457     const KoColorSpace *cs = KoColorSpaceRegistry::instance()->alpha8();
0458     KisPaintDeviceSP dev = new KisPaintDevice(cs);
0459 
0460     const QRect imageRect(0,0,256,256);
0461     dev->fill(QRect(50,50,100,20), KoColor(Qt::white, cs));
0462     dev->fill(QRect(150,50,20,100), KoColor(Qt::white, cs));
0463 
0464     KisDefaultBoundsBaseSP bounds = new TestUtil::TestingTimedDefaultBounds(dev->exactBounds());
0465     dev->setDefaultBounds(bounds);
0466 
0467     TestUtil::checkQImage(dev->convertToQImage(0, imageRect), "convolution_painter_test", "dilate", "initial");
0468 
0469     KisGaussianKernel::applyDilate(dev, imageRect, 10, QBitArray(), 0);
0470 
0471     TestUtil::checkQImage(dev->convertToQImage(0, imageRect), "convolution_painter_test", "dilate", "dilate10");
0472 }
0473 
0474 void KisConvolutionPainterTest::testErode()
0475 {
0476     const KoColorSpace *cs = KoColorSpaceRegistry::instance()->alpha8();
0477     KisPaintDeviceSP dev = new KisPaintDevice(cs);
0478 
0479     const QRect imageRect(0,0,256,256);
0480     dev->fill(QRect(50,50,100,20), KoColor(Qt::white, cs));
0481     dev->fill(QRect(150,50,20,100), KoColor(Qt::white, cs));
0482 
0483     KisDefaultBoundsBaseSP bounds = new TestUtil::TestingTimedDefaultBounds(dev->exactBounds());
0484     dev->setDefaultBounds(bounds);
0485 
0486     TestUtil::checkQImage(dev->convertToQImage(0, imageRect), "convolution_painter_test", "dilate", "initial");
0487 
0488     KisGaussianKernel::applyErodeU8(dev, imageRect, 5, QBitArray(), 0);
0489 
0490     TestUtil::checkQImage(dev->convertToQImage(0, imageRect), "convolution_painter_test", "dilate", "erode5");
0491 }
0492 
0493 #include "kis_edge_detection_kernel.h"
0494 
0495 void KisConvolutionPainterTest::testNormalMap(KisPaintDeviceSP dev, bool useFftw, const QString &prefix)
0496 {
0497    QBitArray channelFlags =
0498        KoColorSpaceRegistry::instance()->rgb8()->channelFlags(true, true);
0499 
0500    KisPainter gc(dev);
0501 
0502 
0503    qreal horizontalRadius = 0.5, verticalRadius = 0.5;
0504 
0505    for(int i = 0; i < 3 ; i++, horizontalRadius+=0.5, verticalRadius+=0.5)
0506    {
0507        QElapsedTimer timer;
0508        timer.start();
0509 
0510        gc.beginTransaction();
0511 
0512        if (( horizontalRadius > 0 ) && ( verticalRadius > 0 )) {
0513 
0514            const QRect applyRect = dev->exactBounds();
0515 
0516            //KisGaussianKernel::applyLoG(dev, applyRect, horizontalRadius, 1.0, channelFlags, 0);
0517 
0518            QVector<int> channelOrder(3);
0519            QVector<bool> channelFlip(3);
0520            channelFlip.fill(false);
0521 
0522            channelOrder[0] = 0;
0523            channelOrder[1] = 1;
0524            channelOrder[2] = 2;
0525 
0526            KisEdgeDetectionKernel::convertToNormalMap(dev, applyRect,
0527                                                       5.67, 5.67,
0528                                                       KisEdgeDetectionKernel::Simple,
0529                                                       0,
0530                                                       channelOrder,
0531                                                       channelFlip,
0532                                                       channelFlags,
0533                                                       0,
0534                                                       useFftw);
0535 
0536            QImage result = dev->convertToQImage(0, applyRect.x(), applyRect.y(), applyRect.width(), applyRect.height());
0537 
0538            QString engine = useFftw ? "fftw" : "spatial";
0539            QString testCaseName = QString("test_%1_%2_%3.png").arg(horizontalRadius).arg(verticalRadius).arg(engine);
0540 
0541            QVERIFY(TestUtil::checkQImage(result,
0542                                          "convolution_painter_test",
0543                                          QString("normalmap_") + prefix,
0544                                          testCaseName, 1));
0545 
0546            gc.revertTransaction();
0547        }
0548        qDebug() << "Elapsed time:" << timer.elapsed() << "ms";
0549     }
0550 }
0551 
0552 void KisConvolutionPainterTest::testNormalMap(bool useFftw)
0553 {
0554     KisPaintDeviceSP dev = new KisPaintDevice(KoColorSpaceRegistry::instance()->rgb8());
0555 
0556     KoColor c(Qt::yellow, dev->colorSpace());
0557 
0558     for (int i = 0; i < 50; i++) {
0559         quint8 baseOpacity = 75;
0560         KoColor c(Qt::magenta, dev->colorSpace());
0561 
0562         for (int j = 0; j <= 6; j++) {
0563             c.setOpacity(static_cast<quint8>(baseOpacity + 30 * j));
0564             dev->setPixel(i + j, i, c);
0565         }
0566     }
0567 
0568     KisDefaultBoundsBaseSP bounds = new TestUtil::TestingTimedDefaultBounds(dev->exactBounds());
0569     dev->setDefaultBounds(bounds);
0570 
0571     testNormalMap(dev, useFftw, "reduced");
0572 }
0573 
0574 void KisConvolutionPainterTest::testNormalMapSpatial()
0575 {
0576     testNormalMap(false);
0577 }
0578 
0579 void KisConvolutionPainterTest::testNormalMapFFTW()
0580 {
0581     testNormalMap(true);
0582 }
0583 
0584 KISTEST_MAIN(KisConvolutionPainterTest)