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)