QGIS API Documentation 4.3.0-Master (0de80482b60)
Loading...
Searching...
No Matches
qgsimageoperation.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsimageoperation.cpp
3 ----------------------
4 begin : January 2015
5 copyright : (C) 2015 by Nyall Dawson
6 email : nyall.dawson@gmail.com
7 ***************************************************************************/
8
9/***************************************************************************
10 * *
11 * This program is free software; you can redistribute it and/or modify *
12 * it under the terms of the GNU General Public License as published by *
13 * the Free Software Foundation; either version 2 of the License, or *
14 * (at your option) any later version. *
15 * *
16 ***************************************************************************/
17
18#include "qgsimageoperation.h"
19
20#include <stack>
21
22#include "qgis.h"
23#include "qgscolorramp.h"
24#include "qgsfeedback.h"
25#include "qgslogger.h"
26
27#include <QColor>
28#include <QPainter>
29#include <QString>
30#include <QtConcurrentMap>
31
32using namespace Qt::StringLiterals;
33
34//determined via trial-and-error. Could possibly be optimised, or varied
35//depending on the image size.
36#define BLOCK_THREADS 16
37
38#define INF 1E20
39
41
42template<typename PixelOperation> void QgsImageOperation::runPixelOperation( QImage &image, PixelOperation &operation, QgsFeedback *feedback )
43{
44 if ( static_cast< qgssize >( image.height() ) * image.width() < 100000 )
45 {
46 //small image, don't multithread
47 //this threshold was determined via testing various images
48 runPixelOperationOnWholeImage( image, operation, feedback );
49 }
50 else
51 {
52 //large image, multithread operation
53 QgsImageOperation::ProcessBlockUsingPixelOperation<PixelOperation> blockOp( operation, feedback );
54 runBlockOperationInThreads( image, blockOp, QgsImageOperation::ByRow );
55 }
56}
57
58template<typename PixelOperation> void QgsImageOperation::runPixelOperationOnWholeImage( QImage &image, PixelOperation &operation, QgsFeedback *feedback )
59{
60 int height = image.height();
61 int width = image.width();
62 for ( int y = 0; y < height; ++y )
63 {
64 if ( feedback && feedback->isCanceled() )
65 break;
66
67 QRgb *ref = reinterpret_cast< QRgb * >( image.scanLine( y ) );
68 for ( int x = 0; x < width; ++x )
69 {
70 operation( ref[x], x, y );
71 }
72 }
73}
74
75//rect operations
76
77template<typename RectOperation> void QgsImageOperation::runRectOperation( QImage &image, RectOperation &operation )
78{
79 //possibly could be tweaked for rect operations
80 if ( static_cast< qgssize >( image.height() ) * image.width() < 100000 )
81 {
82 //small image, don't multithread
83 //this threshold was determined via testing various images
84 runRectOperationOnWholeImage( image, operation );
85 }
86 else
87 {
88 //large image, multithread operation
89 runBlockOperationInThreads( image, operation, ByRow );
90 }
91}
92
93template<class RectOperation> void QgsImageOperation::runRectOperationOnWholeImage( QImage &image, RectOperation &operation )
94{
95 ImageBlock fullImage;
96 fullImage.beginLine = 0;
97 fullImage.endLine = image.height();
98 fullImage.lineLength = image.width();
99 fullImage.image = &image;
100
101 operation( fullImage );
102}
103
104//linear operations
105
106template<typename LineOperation> void QgsImageOperation::runLineOperation( QImage &image, LineOperation &operation, QgsFeedback *feedback )
107{
108 //possibly could be tweaked for rect operations
109 if ( static_cast< qgssize >( image.height() ) * image.width() < 100000 )
110 {
111 //small image, don't multithread
112 //this threshold was determined via testing various images
113 runLineOperationOnWholeImage( image, operation, feedback );
114 }
115 else
116 {
117 //large image, multithread operation
118 QgsImageOperation::ProcessBlockUsingLineOperation<LineOperation> blockOp( operation );
119 runBlockOperationInThreads( image, blockOp, operation.direction() );
120 }
121}
122
123template<class LineOperation> void QgsImageOperation::runLineOperationOnWholeImage( QImage &image, LineOperation &operation, QgsFeedback *feedback )
124{
125 int height = image.height();
126 int width = image.width();
127
128 //do something with whole lines
129 int bpl = image.bytesPerLine();
130 if ( operation.direction() == ByRow )
131 {
132 for ( int y = 0; y < height; ++y )
133 {
134 if ( feedback && feedback->isCanceled() )
135 break;
136
137 QRgb *ref = reinterpret_cast< QRgb * >( image.scanLine( y ) );
138 operation( ref, width, bpl );
139 }
140 }
141 else
142 {
143 //by column
144 unsigned char *ref = image.scanLine( 0 );
145 for ( int x = 0; x < width; ++x, ref += 4 )
146 {
147 if ( feedback && feedback->isCanceled() )
148 break;
149
150 operation( reinterpret_cast< QRgb * >( ref ), height, bpl );
151 }
152 }
153}
154
155
156//multithreaded block processing
157
158template<typename BlockOperation> void QgsImageOperation::runBlockOperationInThreads( QImage &image, BlockOperation &operation, LineOperationDirection direction )
159{
160 QList< ImageBlock > blocks;
161 unsigned int height = image.height();
162 unsigned int width = image.width();
163
164 unsigned int blockDimension1 = ( direction == QgsImageOperation::ByRow ) ? height : width;
165 unsigned int blockDimension2 = ( direction == QgsImageOperation::ByRow ) ? width : height;
166
167 //chunk image up into vertical blocks
168 blocks.reserve( BLOCK_THREADS );
169 unsigned int begin = 0;
170 unsigned int blockLen = blockDimension1 / BLOCK_THREADS;
171 for ( unsigned int block = 0; block < BLOCK_THREADS; ++block, begin += blockLen )
172 {
173 ImageBlock newBlock;
174 newBlock.beginLine = begin;
175 //make sure last block goes to end of image
176 newBlock.endLine = block < ( BLOCK_THREADS - 1 ) ? begin + blockLen : blockDimension1;
177 newBlock.lineLength = blockDimension2;
178 newBlock.image = &image;
179 blocks << newBlock;
180 }
181
182 //process blocks
183 QtConcurrent::blockingMap( blocks, operation );
184}
185
186
188
189//
190//operation specific code
191//
192
193//grayscale
194
195void QgsImageOperation::convertToGrayscale( QImage &image, const GrayscaleMode mode, QgsFeedback *feedback )
196{
197 if ( mode == GrayscaleOff )
198 {
199 return;
200 }
201
202 image.detach();
203 GrayscalePixelOperation operation( mode );
204 runPixelOperation( image, operation, feedback );
205}
206
207void QgsImageOperation::GrayscalePixelOperation::operator()( QRgb &rgb, const int x, const int y ) const
208{
209 Q_UNUSED( x )
210 Q_UNUSED( y )
211 switch ( mMode )
212 {
213 case GrayscaleOff:
214 return;
216 grayscaleLuminosityOp( rgb );
217 return;
218 case GrayscaleAverage:
219 grayscaleAverageOp( rgb );
220 return;
222 default:
223 grayscaleLightnessOp( rgb );
224 return;
225 }
226}
227
228void QgsImageOperation::grayscaleLightnessOp( QRgb &rgb )
229{
230 int red = qRed( rgb );
231 int green = qGreen( rgb );
232 int blue = qBlue( rgb );
233
234 int min = std::min( std::min( red, green ), blue );
235 int max = std::max( std::max( red, green ), blue );
236
237 int lightness = std::min( ( min + max ) / 2, 255 );
238 rgb = qRgba( lightness, lightness, lightness, qAlpha( rgb ) );
239}
240
241void QgsImageOperation::grayscaleLuminosityOp( QRgb &rgb )
242{
243 int luminosity = 0.21 * qRed( rgb ) + 0.72 * qGreen( rgb ) + 0.07 * qBlue( rgb );
244 rgb = qRgba( luminosity, luminosity, luminosity, qAlpha( rgb ) );
245}
246
247void QgsImageOperation::grayscaleAverageOp( QRgb &rgb )
248{
249 int average = ( qRed( rgb ) + qGreen( rgb ) + qBlue( rgb ) ) / 3;
250 rgb = qRgba( average, average, average, qAlpha( rgb ) );
251}
252
253
254//brightness/contrast
255
256void QgsImageOperation::adjustBrightnessContrast( QImage &image, const int brightness, const double contrast, QgsFeedback *feedback )
257{
258 image.detach();
259 BrightnessContrastPixelOperation operation( brightness, contrast );
260 runPixelOperation( image, operation, feedback );
261}
262
263void QgsImageOperation::BrightnessContrastPixelOperation::operator()( QRgb &rgb, const int x, const int y ) const
264{
265 Q_UNUSED( x )
266 Q_UNUSED( y )
267 int red = adjustColorComponent( qRed( rgb ), mBrightness, mContrast );
268 int blue = adjustColorComponent( qBlue( rgb ), mBrightness, mContrast );
269 int green = adjustColorComponent( qGreen( rgb ), mBrightness, mContrast );
270 rgb = qRgba( red, green, blue, qAlpha( rgb ) );
271}
272
273int QgsImageOperation::adjustColorComponent( int colorComponent, int brightness, double contrastFactor )
274{
275 return std::clamp( static_cast< int >( ( ( ( ( ( colorComponent / 255.0 ) - 0.5 ) * contrastFactor ) + 0.5 ) * 255 ) + brightness ), 0, 255 );
276}
277
278//hue/saturation
279
280void QgsImageOperation::adjustHueSaturation( QImage &image, const double saturation, const QColor &colorizeColor, const double colorizeStrength, QgsFeedback *feedback )
281{
282 image.detach();
283 HueSaturationPixelOperation operation( saturation, colorizeColor.isValid() && colorizeStrength > 0.0, colorizeColor.hue(), colorizeColor.saturation(), colorizeStrength );
284 runPixelOperation( image, operation, feedback );
285}
286
287void QgsImageOperation::HueSaturationPixelOperation::operator()( QRgb &rgb, const int x, const int y ) const
288{
289 Q_UNUSED( x )
290 Q_UNUSED( y )
291 QColor tmpColor( rgb );
292 int h, s, l;
293 tmpColor.getHsl( &h, &s, &l );
294
295 if ( mSaturation < 1.0 )
296 {
297 // Lowering the saturation. Use a simple linear relationship
298 s = std::min( static_cast< int >( s * mSaturation ), 255 );
299 }
300 else if ( mSaturation > 1.0 )
301 {
302 // Raising the saturation. Use a saturation curve to prevent
303 // clipping at maximum saturation with ugly results.
304 s = std::min( static_cast< int >( 255. * ( 1 - std::pow( 1 - ( s / 255. ), std::pow( mSaturation, 2 ) ) ) ), 255 );
305 }
306
307 if ( mColorize )
308 {
309 h = mColorizeHue;
310 s = mColorizeSaturation;
311 if ( mColorizeStrength < 1.0 )
312 {
313 //get rgb for colorized color
314 QColor colorizedColor = QColor::fromHsl( h, s, l );
315 int colorizedR, colorizedG, colorizedB;
316 colorizedColor.getRgb( &colorizedR, &colorizedG, &colorizedB );
317
318 // Now, linearly scale by colorize strength
319 int r = mColorizeStrength * colorizedR + ( 1 - mColorizeStrength ) * tmpColor.red();
320 int g = mColorizeStrength * colorizedG + ( 1 - mColorizeStrength ) * tmpColor.green();
321 int b = mColorizeStrength * colorizedB + ( 1 - mColorizeStrength ) * tmpColor.blue();
322
323 rgb = qRgba( r, g, b, qAlpha( rgb ) );
324 return;
325 }
326 }
327
328 tmpColor.setHsl( h, s, l, qAlpha( rgb ) );
329 rgb = tmpColor.rgba();
330}
331
332//multiply opacity
333
334void QgsImageOperation::multiplyOpacity( QImage &image, const double factor, QgsFeedback *feedback )
335{
336 if ( qgsDoubleNear( factor, 1.0 ) )
337 {
338 //no change
339 return;
340 }
341 else if ( factor < 1.0 )
342 {
343 //decreasing opacity - we can use the faster DestinationIn composition mode
344 //to reduce the alpha channel
345 QColor transparentFillColor = QColor( 0, 0, 0, 255 * factor );
346 if ( image.format() == QImage::Format_Indexed8 )
347 image = image.convertToFormat( QImage::Format_ARGB32 );
348 else
349 image.detach();
350
351 QPainter painter( &image );
352 painter.setCompositionMode( QPainter::CompositionMode_DestinationIn );
353 painter.fillRect( 0, 0, image.width(), image.height(), transparentFillColor );
354 painter.end();
355 }
356 else
357 {
358 //increasing opacity - run this as a pixel operation for multithreading
359 image.detach();
360 MultiplyOpacityPixelOperation operation( factor );
361 runPixelOperation( image, operation, feedback );
362 }
363}
364
365void QgsImageOperation::MultiplyOpacityPixelOperation::operator()( QRgb &rgb, const int x, const int y ) const
366{
367 Q_UNUSED( x )
368 Q_UNUSED( y )
369 rgb = qRgba( qRed( rgb ), qGreen( rgb ), qBlue( rgb ), std::clamp( std::round( mFactor * qAlpha( rgb ) ), 0.0, 255.0 ) );
370}
371
372// overlay color
373
374void QgsImageOperation::overlayColor( QImage &image, const QColor &color )
375{
376 QColor opaqueColor = color;
377 opaqueColor.setAlpha( 255 );
378
379 //use QPainter SourceIn composition mode to overlay color (fast)
380 //this retains image's alpha channel but replaces color
381 image.detach();
382 QPainter painter( &image );
383 painter.setCompositionMode( QPainter::CompositionMode_SourceIn );
384 painter.fillRect( 0, 0, image.width(), image.height(), opaqueColor );
385 painter.end();
386}
387
388// distance transform
389
390void QgsImageOperation::distanceTransform( QImage &image, const DistanceTransformProperties &properties, QgsFeedback *feedback )
391{
392 if ( !properties.ramp )
393 {
394 QgsDebugError( u"no color ramp specified for distance transform"_s );
395 return;
396 }
397
398 //first convert to 1 bit alpha mask array
399 std::unique_ptr<double[]> array( new double[static_cast< qgssize >( image.width() ) * image.height()] );
400 if ( feedback && feedback->isCanceled() )
401 return;
402
403 image.detach();
404 ConvertToArrayPixelOperation convertToArray( image.width(), array.get(), properties.shadeExterior );
405 runPixelOperation( image, convertToArray, feedback );
406 if ( feedback && feedback->isCanceled() )
407 return;
408
409 //calculate distance transform (single threaded only)
410 distanceTransform2d( array.get(), image.width(), image.height(), feedback );
411 if ( feedback && feedback->isCanceled() )
412 return;
413
414 double spread;
415 if ( properties.useMaxDistance )
416 {
417 spread = std::sqrt( maxValueInDistanceTransformArray( array.get(), image.width() * image.height() ) );
418 }
419 else
420 {
421 spread = properties.spread;
422 }
423
424 if ( feedback && feedback->isCanceled() )
425 return;
426
427 //shade distance transform
428 ShadeFromArrayOperation shadeFromArray( image.width(), array.get(), spread, properties );
429 runPixelOperation( image, shadeFromArray, feedback );
430}
431
432void QgsImageOperation::ConvertToArrayPixelOperation::operator()( QRgb &rgb, const int x, const int y )
433{
434 qgssize idx = y * static_cast< qgssize >( mWidth ) + x;
435 if ( mExterior )
436 {
437 if ( qAlpha( rgb ) > 0 )
438 {
439 //opaque pixel, so zero distance
440 mArray[idx] = 1 - qAlpha( rgb ) / 255.0;
441 }
442 else
443 {
444 //transparent pixel, so initially set distance as infinite
445 mArray[idx] = INF;
446 }
447 }
448 else
449 {
450 //TODO - fix this for semi-transparent pixels
451 if ( qAlpha( rgb ) == 255 )
452 {
453 mArray[idx] = INF;
454 }
455 else
456 {
457 mArray[idx] = 0;
458 }
459 }
460}
461
462//fast distance transform code, adapted from http://cs.brown.edu/~pff/dt/
463
464/* distance transform of a 1d function using squared distance */
465void QgsImageOperation::distanceTransform1d( double *f, int n, int *v, double *z, double *d )
466{
467 int k = 0;
468 v[0] = 0;
469 z[0] = -INF;
470 z[1] = +INF;
471 for ( int q = 1; q <= n - 1; q++ )
472 {
473 double s = ( ( f[q] + q * q ) - ( f[v[k]] + ( v[k] * v[k] ) ) ) / ( 2 * q - 2 * v[k] );
474 while ( s <= z[k] )
475 {
476 k--;
477 s = ( ( f[q] + q * q ) - ( f[v[k]] + ( v[k] * v[k] ) ) ) / ( 2 * q - 2 * v[k] );
478 }
479 k++;
480 v[k] = q;
481 z[k] = s;
482 z[k + 1] = +INF;
483 }
484
485 k = 0;
486 for ( int q = 0; q <= n - 1; q++ )
487 {
488 while ( z[k + 1] < q )
489 k++;
490 d[q] = ( q - v[k] ) * ( q - v[k] ) + f[v[k]];
491 }
492}
493
494double QgsImageOperation::maxValueInDistanceTransformArray( const double *array, const unsigned int size )
495{
496 double dtMaxValue = array[0];
497 for ( unsigned int i = 1; i < size; ++i )
498 {
499 if ( array[i] > dtMaxValue )
500 {
501 dtMaxValue = array[i];
502 }
503 }
504 return dtMaxValue;
505}
506
507/* distance transform of 2d function using squared distance */
508void QgsImageOperation::distanceTransform2d( double *im, int width, int height, QgsFeedback *feedback )
509{
510 int maxDimension = std::max( width, height );
511
512 std::unique_ptr<double[]> f( new double[maxDimension] );
513 std::unique_ptr<int[]> v( new int[maxDimension] );
514 std::unique_ptr<double[]> z( new double[maxDimension + 1] );
515 std::unique_ptr<double[]> d( new double[maxDimension] );
516
517 // transform along columns
518 for ( int x = 0; x < width; x++ )
519 {
520 if ( feedback && feedback->isCanceled() )
521 break;
522
523 for ( int y = 0; y < height; y++ )
524 {
525 f[y] = im[x + y * width];
526 }
527 distanceTransform1d( f.get(), height, v.get(), z.get(), d.get() );
528 for ( int y = 0; y < height; y++ )
529 {
530 im[x + y * width] = d[y];
531 }
532 }
533
534 // transform along rows
535 for ( int y = 0; y < height; y++ )
536 {
537 if ( feedback && feedback->isCanceled() )
538 break;
539
540 for ( int x = 0; x < width; x++ )
541 {
542 f[x] = im[x + y * width];
543 }
544 distanceTransform1d( f.get(), width, v.get(), z.get(), d.get() );
545 for ( int x = 0; x < width; x++ )
546 {
547 im[x + y * width] = d[x];
548 }
549 }
550}
551
552void QgsImageOperation::ShadeFromArrayOperation::operator()( QRgb &rgb, const int x, const int y )
553{
554 if ( !mProperties.ramp )
555 return;
556
557 if ( qgsDoubleNear( mSpread, 0.0 ) )
558 {
559 rgb = mProperties.ramp->color( 1.0 ).rgba();
560 return;
561 }
562
563 int idx = y * mWidth + x;
564
565 //values are distance squared
566 double squaredVal = mArray[idx];
567 if ( squaredVal > mSpreadSquared )
568 {
569 rgb = Qt::transparent;
570 return;
571 }
572
573 double distance = std::sqrt( squaredVal );
574 double val = distance / mSpread;
575 QColor rampColor = mProperties.ramp->color( val );
576
577 if ( ( mProperties.shadeExterior && distance > mSpread - 1 ) )
578 {
579 //fade off final pixel to antialias edge
580 double alphaMultiplyFactor = mSpread - distance;
581 rampColor.setAlpha( rampColor.alpha() * alphaMultiplyFactor );
582 }
583 rgb = rampColor.rgba();
584}
585
586//stack blur
587
588void QgsImageOperation::stackBlur( QImage &image, const int radius, const bool alphaOnly, QgsFeedback *feedback )
589{
590 // culled from Qt's qpixmapfilter.cpp, see: http://www.qtcentre.org/archive/index.php/t-26534.html
591 int tab[] = { 14, 10, 8, 6, 5, 5, 4, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2 };
592 int alpha = ( radius < 1 ) ? 16 : ( radius > 17 ) ? 1 : tab[radius - 1];
593
594 int i1 = 0;
595 int i2 = 3;
596
597 //ensure correct source format.
598 QImage::Format originalFormat = image.format();
599 QImage *pImage = &image;
600 std::unique_ptr< QImage> convertedImage;
601 if ( !alphaOnly && originalFormat != QImage::Format_ARGB32_Premultiplied )
602 {
603 convertedImage = std::make_unique< QImage >( image.convertToFormat( QImage::Format_ARGB32_Premultiplied ) );
604 pImage = convertedImage.get();
605 }
606 else if ( alphaOnly && originalFormat != QImage::Format_ARGB32 )
607 {
608 convertedImage = std::make_unique< QImage >( image.convertToFormat( QImage::Format_ARGB32 ) );
609 pImage = convertedImage.get();
610 }
611 else
612 {
613 image.detach();
614 }
615
616 if ( feedback && feedback->isCanceled() )
617 return;
618
619 if ( alphaOnly )
620 i1 = i2 = ( QSysInfo::ByteOrder == QSysInfo::BigEndian ? 0 : 3 );
621
622 StackBlurLineOperation topToBottomBlur( alpha, QgsImageOperation::ByColumn, true, i1, i2, feedback );
623 runLineOperation( *pImage, topToBottomBlur, feedback );
624
625 if ( feedback && feedback->isCanceled() )
626 return;
627
628 StackBlurLineOperation leftToRightBlur( alpha, QgsImageOperation::ByRow, true, i1, i2, feedback );
629 runLineOperation( *pImage, leftToRightBlur, feedback );
630
631 if ( feedback && feedback->isCanceled() )
632 return;
633
634 StackBlurLineOperation bottomToTopBlur( alpha, QgsImageOperation::ByColumn, false, i1, i2, feedback );
635 runLineOperation( *pImage, bottomToTopBlur, feedback );
636
637 if ( feedback && feedback->isCanceled() )
638 return;
639
640 StackBlurLineOperation rightToLeftBlur( alpha, QgsImageOperation::ByRow, false, i1, i2, feedback );
641 runLineOperation( *pImage, rightToLeftBlur, feedback );
642
643 if ( feedback && feedback->isCanceled() )
644 return;
645
646 if ( pImage->format() != originalFormat )
647 {
648 image = pImage->convertToFormat( originalFormat );
649 }
650}
651
652//gaussian blur
653
654QImage *QgsImageOperation::gaussianBlur( QImage &image, const int radius, QgsFeedback *feedback )
655{
656 int width = image.width();
657 int height = image.height();
658
659 if ( radius <= 0 )
660 {
661 //just make an unchanged copy
662 QImage *copy = new QImage( image.copy() );
663 return copy;
664 }
665
666 std::unique_ptr<double[]> kernel( createGaussianKernel( radius ) );
667 if ( feedback && feedback->isCanceled() )
668 return new QImage();
669
670 //ensure correct source format.
671 QImage::Format originalFormat = image.format();
672 QImage *pImage = &image;
673 std::unique_ptr< QImage> convertedImage;
674 if ( originalFormat != QImage::Format_ARGB32_Premultiplied )
675 {
676 convertedImage = std::make_unique< QImage >( image.convertToFormat( QImage::Format_ARGB32_Premultiplied ) );
677 pImage = convertedImage.get();
678 }
679 else
680 {
681 image.detach();
682 }
683 if ( feedback && feedback->isCanceled() )
684 return new QImage();
685
686 //blur along rows
687 QImage xBlurImage = QImage( width, height, QImage::Format_ARGB32_Premultiplied );
688 GaussianBlurOperation rowBlur( radius, QgsImageOperation::ByRow, &xBlurImage, kernel.get(), feedback );
689 runRectOperation( *pImage, rowBlur );
690
691 if ( feedback && feedback->isCanceled() )
692 return new QImage();
693
694 //blur along columns
695 auto yBlurImage = std::make_unique< QImage >( width, height, QImage::Format_ARGB32_Premultiplied );
696 GaussianBlurOperation colBlur( radius, QgsImageOperation::ByColumn, yBlurImage.get(), kernel.get(), feedback );
697 runRectOperation( xBlurImage, colBlur );
698
699 if ( feedback && feedback->isCanceled() )
700 return new QImage();
701
702 kernel.reset();
703
704 if ( originalFormat != QImage::Format_ARGB32_Premultiplied )
705 {
706 return new QImage( yBlurImage->convertToFormat( originalFormat ) );
707 }
708
709 return yBlurImage.release();
710}
711
712void QgsImageOperation::GaussianBlurOperation::operator()( QgsImageOperation::ImageBlock &block )
713{
714 if ( mFeedback && mFeedback->isCanceled() )
715 return;
716
717 int width = block.image->width();
718 int height = block.image->height();
719 int sourceBpl = block.image->bytesPerLine();
720
721 unsigned char *outputLineRef = mDestImage->scanLine( block.beginLine );
722 QRgb *destRef = nullptr;
723 if ( mDirection == ByRow )
724 {
725 unsigned char *sourceFirstLine = block.image->scanLine( 0 );
726 unsigned char *sourceRef;
727
728 //blur along rows
729 for ( unsigned int y = block.beginLine; y < block.endLine; ++y, outputLineRef += mDestImageBpl )
730 {
731 if ( mFeedback && mFeedback->isCanceled() )
732 break;
733
734 sourceRef = sourceFirstLine;
735 destRef = reinterpret_cast< QRgb * >( outputLineRef );
736 for ( int x = 0; x < width; ++x, ++destRef, sourceRef += 4 )
737 {
738 if ( mFeedback && mFeedback->isCanceled() )
739 break;
740
741 *destRef = gaussianBlurVertical( y, sourceRef, sourceBpl, height );
742 }
743 }
744 }
745 else
746 {
747 unsigned char *sourceRef = block.image->scanLine( block.beginLine );
748 for ( unsigned int y = block.beginLine; y < block.endLine; ++y, outputLineRef += mDestImageBpl, sourceRef += sourceBpl )
749 {
750 if ( mFeedback && mFeedback->isCanceled() )
751 break;
752
753 destRef = reinterpret_cast< QRgb * >( outputLineRef );
754 for ( int x = 0; x < width; ++x, ++destRef )
755 {
756 if ( mFeedback && mFeedback->isCanceled() )
757 break;
758
759 *destRef = gaussianBlurHorizontal( x, sourceRef, width );
760 }
761 }
762 }
763}
764
765inline QRgb QgsImageOperation::GaussianBlurOperation::gaussianBlurVertical( const int posy, unsigned char *sourceFirstLine, const int sourceBpl, const int height ) const
766{
767 double r = 0;
768 double b = 0;
769 double g = 0;
770 double a = 0;
771 int y;
772 unsigned char *ref;
773
774 for ( int i = 0; i <= mRadius * 2; ++i )
775 {
776 y = std::clamp( posy + ( i - mRadius ), 0, height - 1 );
777 ref = sourceFirstLine + static_cast< std::size_t >( sourceBpl ) * y;
778
779 QRgb *refRgb = reinterpret_cast< QRgb * >( ref );
780 r += mKernel[i] * qRed( *refRgb );
781 g += mKernel[i] * qGreen( *refRgb );
782 b += mKernel[i] * qBlue( *refRgb );
783 a += mKernel[i] * qAlpha( *refRgb );
784 }
785
786 return qRgba( r, g, b, a );
787}
788
789inline QRgb QgsImageOperation::GaussianBlurOperation::gaussianBlurHorizontal( const int posx, unsigned char *sourceFirstLine, const int width ) const
790{
791 double r = 0;
792 double b = 0;
793 double g = 0;
794 double a = 0;
795 int x;
796 unsigned char *ref;
797
798 for ( int i = 0; i <= mRadius * 2; ++i )
799 {
800 x = std::clamp( posx + ( i - mRadius ), 0, width - 1 );
801 ref = sourceFirstLine + x * 4;
802
803 QRgb *refRgb = reinterpret_cast< QRgb * >( ref );
804 r += mKernel[i] * qRed( *refRgb );
805 g += mKernel[i] * qGreen( *refRgb );
806 b += mKernel[i] * qBlue( *refRgb );
807 a += mKernel[i] * qAlpha( *refRgb );
808 }
809
810 return qRgba( r, g, b, a );
811}
812
813
814double *QgsImageOperation::createGaussianKernel( const int radius )
815{
816 double *kernel = new double[radius * 2 + 1];
817 double sigma = radius / 3.0;
818 double twoSigmaSquared = 2 * sigma * sigma;
819 double coefficient = 1.0 / std::sqrt( M_PI * twoSigmaSquared );
820 double expCoefficient = -1.0 / twoSigmaSquared;
821
822 double sum = 0;
823 double result;
824 for ( int i = 0; i <= radius; ++i )
825 {
826 result = coefficient * std::exp( i * i * expCoefficient );
827 kernel[radius - i] = result;
828 sum += result;
829 if ( i > 0 )
830 {
831 kernel[radius + i] = result;
832 sum += result;
833 }
834 }
835 //normalize
836 for ( int i = 0; i <= radius * 2; ++i )
837 {
838 kernel[i] /= sum;
839 }
840 return kernel;
841}
842
843
844// flip
845
847{
848 image.detach();
849 FlipLineOperation flipOperation( type == QgsImageOperation::FlipHorizontal ? QgsImageOperation::ByRow : QgsImageOperation::ByColumn );
850 runLineOperation( image, flipOperation );
851}
852
853QRect QgsImageOperation::nonTransparentImageRect( const QImage &image, QSize minSize, bool center )
854{
855 int width = image.width();
856 int height = image.height();
857 int xmin = width;
858 int xmax = 0;
859 int ymin = height;
860 int ymax = 0;
861
862 // scan down till we hit something
863 for ( int y = 0; y < height; ++y )
864 {
865 bool found = false;
866 const QRgb *imgScanline = reinterpret_cast< const QRgb * >( image.constScanLine( y ) );
867 for ( int x = 0; x < width; ++x )
868 {
869 if ( qAlpha( imgScanline[x] ) )
870 {
871 ymin = y;
872 ymax = y;
873 xmin = x;
874 xmax = x;
875 found = true;
876 break;
877 }
878 }
879 if ( found )
880 break;
881 }
882
883 //scan up till we hit something
884 for ( int y = height - 1; y >= ymin; --y )
885 {
886 bool found = false;
887 const QRgb *imgScanline = reinterpret_cast< const QRgb * >( image.constScanLine( y ) );
888 for ( int x = 0; x < width; ++x )
889 {
890 if ( qAlpha( imgScanline[x] ) )
891 {
892 ymax = y;
893 xmin = std::min( xmin, x );
894 xmax = std::max( xmax, x );
895 found = true;
896 break;
897 }
898 }
899 if ( found )
900 break;
901 }
902
903 //scan left to right till we hit something, using a refined y region
904 for ( int y = ymin; y <= ymax; ++y )
905 {
906 const QRgb *imgScanline = reinterpret_cast< const QRgb * >( image.constScanLine( y ) );
907 for ( int x = 0; x < xmin; ++x )
908 {
909 if ( qAlpha( imgScanline[x] ) )
910 {
911 xmin = x;
912 break;
913 }
914 }
915 }
916
917 //scan right to left till we hit something, using the refined y region
918 for ( int y = ymin; y <= ymax; ++y )
919 {
920 const QRgb *imgScanline = reinterpret_cast< const QRgb * >( image.constScanLine( y ) );
921 for ( int x = width - 1; x > xmax; --x )
922 {
923 if ( qAlpha( imgScanline[x] ) )
924 {
925 xmax = x;
926 break;
927 }
928 }
929 }
930
931 if ( minSize.isValid() )
932 {
933 if ( xmax - xmin < minSize.width() ) // centers image on x
934 {
935 xmin = std::max( ( xmax + xmin ) / 2 - minSize.width() / 2, 0 );
936 xmax = xmin + minSize.width();
937 }
938 if ( ymax - ymin < minSize.height() ) // centers image on y
939 {
940 ymin = std::max( ( ymax + ymin ) / 2 - minSize.height() / 2, 0 );
941 ymax = ymin + minSize.height();
942 }
943 }
944 if ( center )
945 {
946 // recompute min and max to center image
947 const int dx = std::max( std::abs( xmax - width / 2 ), std::abs( xmin - width / 2 ) );
948 const int dy = std::max( std::abs( ymax - height / 2 ), std::abs( ymin - height / 2 ) );
949 xmin = std::max( 0, width / 2 - dx );
950 xmax = std::min( width, width / 2 + dx );
951 ymin = std::max( 0, height / 2 - dy );
952 ymax = std::min( height, height / 2 + dy );
953 }
954
955 return QRect( xmin, ymin, xmax - xmin, ymax - ymin );
956}
957
958QImage QgsImageOperation::cropTransparent( const QImage &image, QSize minSize, bool center )
959{
960 return image.copy( QgsImageOperation::nonTransparentImageRect( image, minSize, center ) );
961}
962
963bool QgsImageOperation::isBlankImage( const QImage &image )
964{
965 if ( image.isNull() )
966 return true;
967
968 // if image doesn't have alpha channel, we can shortcut
969 switch ( image.format() )
970 {
971 case QImage::Format_Invalid:
972 case QImage::Format_Mono:
973 case QImage::Format_MonoLSB:
974 case QImage::Format_Indexed8:
975 case QImage::Format_RGB32:
976 case QImage::Format_RGB16:
977 case QImage::Format_RGB666:
978 case QImage::Format_RGB555:
979 case QImage::Format_RGB888:
980 case QImage::Format_RGB444:
981 case QImage::Format_RGBX8888:
982 case QImage::Format_BGR30:
983 case QImage::Format_RGB30:
984 case QImage::Format_Grayscale8:
985 case QImage::Format_RGBX64:
986 case QImage::Format_Grayscale16:
987 case QImage::Format_BGR888:
988 case QImage::Format_RGBX16FPx4:
989 case QImage::Format_RGBX32FPx4:
990 case QImage::Format_CMYK8888:
991 case QImage::NImageFormats:
992 return false;
993
994 case QImage::Format_ARGB32:
995 case QImage::Format_ARGB32_Premultiplied:
996 case QImage::Format_ARGB8565_Premultiplied:
997 case QImage::Format_ARGB6666_Premultiplied:
998 case QImage::Format_ARGB8555_Premultiplied:
999 case QImage::Format_ARGB4444_Premultiplied:
1000 case QImage::Format_RGBA8888:
1001 case QImage::Format_RGBA8888_Premultiplied:
1002 case QImage::Format_A2BGR30_Premultiplied:
1003 case QImage::Format_A2RGB30_Premultiplied:
1004 case QImage::Format_Alpha8:
1005 case QImage::Format_RGBA64:
1006 case QImage::Format_RGBA64_Premultiplied:
1007 case QImage::Format_RGBA16FPx4:
1008 case QImage::Format_RGBA16FPx4_Premultiplied:
1009 case QImage::Format_RGBA32FPx4:
1010 case QImage::Format_RGBA32FPx4_Premultiplied:
1011 break;
1012 }
1013 const int width = image.width();
1014 const int height = image.height();
1015 const qsizetype bytesPerLine = image.bytesPerLine();
1016 const qsizetype totalPixels = static_cast<qsizetype>( width ) * height;
1017
1018 constexpr uint32_t ALPHA_MASK_32 = 0xFF000000;
1019 constexpr uint64_t ALPHA_MASK_64 = 0xFF000000FF000000ULL;
1020
1021 // optimized check for ARGB32 types:
1022 if ( image.format() == QImage::Format_ARGB32 || image.format() == QImage::Format_ARGB32_Premultiplied )
1023 {
1024 if ( bytesPerLine == static_cast< qsizetype>( width ) * 4 )
1025 {
1026 // contiguous memory check - check 2 pixels at a time
1027 const uint64_t *ptr64 = reinterpret_cast<const uint64_t *>( image.constBits() );
1028 const qsizetype count64 = totalPixels / 2;
1029 for ( qsizetype i = 0; i < count64; ++i )
1030 {
1031 // check only alpha channel
1032 if ( ( ptr64[i] & ALPHA_MASK_64 ) != 0 )
1033 return false;
1034 }
1035 // handle remaining odd pixel if totalPixels is odd
1036 if ( totalPixels % 2 != 0 )
1037 {
1038 const uint32_t *ptr32 = reinterpret_cast<const uint32_t *>( image.constBits() );
1039 if ( ( ptr32[totalPixels - 1] & ALPHA_MASK_32 ) != 0 )
1040 return false;
1041 }
1042 }
1043 else
1044 {
1045 // line-by-line fallback if stride contains padding
1046 for ( int y = 0; y < height; ++y )
1047 {
1048 const uint32_t *line = reinterpret_cast<const uint32_t *>( image.constScanLine( y ) );
1049 for ( int x = 0; x < width; ++x )
1050 {
1051 // check only alpha channel
1052 if ( ( line[x] & ALPHA_MASK_32 ) != 0 )
1053 return false;
1054 }
1055 }
1056 }
1057 return true;
1058 }
1059
1060 // for other image types just convert to ARGB32 and re-test
1061 // TODO (if needed!): add optimized checks for particular formats which are actually in use
1062 QgsDebugError( u"QgsImageOperation::isBlankImage called with non-optimized image format: %1"_s.arg( qgsEnumValueToKey( image.format() ) ) );
1063 return isBlankImage( image.convertToFormat( QImage::Format_ARGB32 ) );
1064}
1065
1066namespace
1067{
1068 inline bool check32BitImage( const QImage &image, uint32_t targetPixel )
1069 {
1070 const int width = image.width();
1071 const int height = image.height();
1072 const qsizetype bytesPerLine = image.bytesPerLine();
1073 const qsizetype totalPixels = static_cast<qsizetype>( width ) * height;
1074
1075 // check if image data is completely contiguous, if so, we can use an optimized check
1076 if ( bytesPerLine == static_cast<qsizetype>( width ) * 4 )
1077 {
1078 const uint64_t *ptr64 = reinterpret_cast<const uint64_t *>( image.constBits() );
1079 const qsizetype count64 = totalPixels / 2;
1080 const uint64_t target64 = ( static_cast<uint64_t>( targetPixel ) << 32 ) | targetPixel;
1081 for ( qsizetype i = 0; i < count64; ++i )
1082 {
1083 if ( ptr64[i] != target64 )
1084 return false;
1085 }
1086 // handle remaining odd pixel if totalPixels is odd
1087 if ( totalPixels % 2 != 0 )
1088 {
1089 const uint32_t *ptr32 = reinterpret_cast<const uint32_t *>( image.constBits() );
1090 if ( ptr32[totalPixels - 1] != targetPixel )
1091 return false;
1092 }
1093 }
1094 else
1095 {
1096 for ( int y = 0; y < height; ++y )
1097 {
1098 const uint32_t *line = reinterpret_cast<const uint32_t *>( image.constScanLine( y ) );
1099 for ( int x = 0; x < width; ++x )
1100 {
1101 if ( line[x] != targetPixel )
1102 return false;
1103 }
1104 }
1105 }
1106 return true;
1107 }
1108} //namespace
1109
1110bool QgsImageOperation::isSingleColor( const QImage &image, const QColor &color )
1111{
1112 if ( image.isNull() )
1113 return false;
1114
1115 switch ( image.format() )
1116 {
1117 case QImage::Format_ARGB32:
1118 return check32BitImage( image, static_cast<uint32_t>( color.rgba() ) );
1119
1120 case QImage::Format_RGB32:
1121 return check32BitImage( image, static_cast<uint32_t>( color.rgb() ) );
1122
1123 case QImage::Format_ARGB32_Premultiplied:
1124 return check32BitImage( image, static_cast<uint32_t>( qPremultiply( color.rgba() ) ) );
1125
1126 default:
1127 break;
1128 }
1129
1130 // for other image types just convert to ARGB32 and re-test
1131 // TODO (if needed!): add optimized checks for particular formats which are actually in use
1132 QgsDebugError( u"QgsImageOperation::isSingleColor called with non-optimized image format: %1"_s.arg( qgsEnumValueToKey( image.format() ) ) );
1133 return isSingleColor( image.convertToFormat( QImage::Format_ARGB32 ), color );
1134}
1135
1136inline bool colorsMatchFloodFill( QRgb c1, QRgb c2, int tolerance )
1137{
1138 if ( tolerance == 0 )
1139 {
1140 return c1 == c2;
1141 }
1142 return std::abs( qRed( c1 ) - qRed( c2 ) ) <= tolerance
1143 && std::abs( qGreen( c1 ) - qGreen( c2 ) ) <= tolerance
1144 && std::abs( qBlue( c1 ) - qBlue( c2 ) ) <= tolerance
1145 && std::abs( qAlpha( c1 ) - qAlpha( c2 ) ) <= tolerance;
1146}
1147
1148QImage QgsImageOperation::floodFill( const QImage &image, const QPoint &startPoint, const QColor &newColor, int tolerance, QgsFeedback *feedback )
1149{
1150 if ( image.isNull() || !image.rect().contains( startPoint ) )
1151 {
1152 return image;
1153 }
1154
1155 QImage resultImage;
1156 if ( image.format() != QImage::Format_ARGB32 && image.format() != QImage::Format_ARGB32_Premultiplied && image.format() != QImage::Format_RGB32 )
1157 {
1158 resultImage = image.convertToFormat( QImage::Format_ARGB32 );
1159 }
1160 else
1161 {
1162 resultImage = image.copy();
1163 }
1164
1165 const QRgb targetColorRgb = resultImage.pixel( startPoint );
1166 const QRgb newColorRgb = newColor.rgba();
1167 if ( colorsMatchFloodFill( targetColorRgb, newColorRgb, tolerance ) )
1168 {
1169 return resultImage;
1170 }
1171
1172 const int width = resultImage.width();
1173 const int height = resultImage.height();
1174
1175 std::stack<QPoint> stack;
1176 stack.push( startPoint );
1177 while ( !stack.empty() )
1178 {
1179 if ( feedback && feedback->isCanceled() )
1180 {
1181 break;
1182 }
1183
1184 const QPoint pt = stack.top();
1185 stack.pop();
1186 const int x = pt.x();
1187 const int y = pt.y();
1188 QRgb *scanline = reinterpret_cast< QRgb * >( resultImage.scanLine( y ) );
1189 int x1 = x;
1190 while ( x1 >= 0 && colorsMatchFloodFill( scanline[x1], targetColorRgb, tolerance ) )
1191 {
1192 x1--;
1193 }
1194 x1++;
1195 int x2 = x;
1196 while ( x2 < width && colorsMatchFloodFill( scanline[x2], targetColorRgb, tolerance ) )
1197 {
1198 scanline[x2] = newColorRgb;
1199 x2++;
1200 }
1201 x2--;
1202
1203 bool spanAbove = false;
1204 bool spanBelow = false;
1205
1206 QRgb *scanlineAbove = ( y > 0 ) ? reinterpret_cast< QRgb * >( resultImage.scanLine( y - 1 ) ) : nullptr;
1207 QRgb *scanlineBelow = ( y < height - 1 ) ? reinterpret_cast< QRgb * >( resultImage.scanLine( y + 1 ) ) : nullptr;
1208 for ( int currX = x1; currX <= x2; currX++ )
1209 {
1210 // check the row above
1211 if ( scanlineAbove )
1212 {
1213 const bool match = colorsMatchFloodFill( scanlineAbove[currX], targetColorRgb, tolerance );
1214 if ( !spanAbove && match )
1215 {
1216 stack.push( QPoint( currX, y - 1 ) );
1217 spanAbove = true;
1218 }
1219 else if ( spanAbove && !match )
1220 {
1221 spanAbove = false;
1222 }
1223 }
1224
1225 // check the row below
1226 if ( scanlineBelow )
1227 {
1228 const bool match = colorsMatchFloodFill( scanlineBelow[currX], targetColorRgb, tolerance );
1229 if ( !spanBelow && match )
1230 {
1231 stack.push( QPoint( currX, y + 1 ) );
1232 spanBelow = true;
1233 }
1234 else if ( spanBelow && !match )
1235 {
1236 spanBelow = false;
1237 }
1238 }
1239 }
1240 }
1241
1242 return resultImage;
1243}
1244
1245void QgsImageOperation::FlipLineOperation::operator()( QRgb *startRef, const int lineLength, const int bytesPerLine ) const
1246{
1247 int increment = ( mDirection == QgsImageOperation::ByRow ) ? 4 : bytesPerLine;
1248
1249 //store temporary line
1250 unsigned char *p = reinterpret_cast< unsigned char * >( startRef );
1251 unsigned char *tempLine = new unsigned char[lineLength * 4];
1252 for ( int i = 0; i < lineLength * 4; ++i, p += increment )
1253 {
1254 tempLine[i++] = *( p++ );
1255 tempLine[i++] = *( p++ );
1256 tempLine[i++] = *( p++ );
1257 tempLine[i] = *( p );
1258 p -= 3;
1259 }
1260
1261 //write values back in reverse order
1262 p = reinterpret_cast< unsigned char * >( startRef );
1263 for ( int i = ( lineLength - 1 ) * 4; i >= 0; i -= 7, p += increment )
1264 {
1265 *( p++ ) = tempLine[i++];
1266 *( p++ ) = tempLine[i++];
1267 *( p++ ) = tempLine[i++];
1268 *( p ) = tempLine[i];
1269 p -= 3;
1270 }
1271
1272 delete[] tempLine;
1273}
Base class for feedback objects to be used for cancellation of something running in a worker thread.
Definition qgsfeedback.h:44
bool isCanceled() const
Tells whether the operation has been canceled already.
Definition qgsfeedback.h:56
static void adjustHueSaturation(QImage &image, double saturation, const QColor &colorizeColor=QColor(), double colorizeStrength=1.0, QgsFeedback *feedback=nullptr)
Alter the hue or saturation of a QImage.
static void multiplyOpacity(QImage &image, double factor, QgsFeedback *feedback=nullptr)
Multiplies opacity of image pixel values by a factor.
static void distanceTransform(QImage &image, const QgsImageOperation::DistanceTransformProperties &properties, QgsFeedback *feedback=nullptr)
Performs a distance transform on the source image and shades the result using a color ramp.
FlipType
Flip operation types.
@ FlipHorizontal
Flip the image horizontally.
static void overlayColor(QImage &image, const QColor &color)
Overlays a color onto an image.
static QImage floodFill(const QImage &image, const QPoint &startPoint, const QColor &newColor, int tolerance=0, QgsFeedback *feedback=nullptr)
Performs a flood fill operation on an image, replacing contiguous areas of the same color.
static void flipImage(QImage &image, FlipType type)
Flips an image horizontally or vertically.
static void adjustBrightnessContrast(QImage &image, int brightness, double contrast, QgsFeedback *feedback=nullptr)
Alter the brightness or contrast of a QImage.
static bool isBlankImage(const QImage &image)
Tests whether an image is completely blank, i.e.
static QImage * gaussianBlur(QImage &image, int radius, QgsFeedback *feedback=nullptr)
Performs a gaussian blur on an image.
static bool isSingleColor(const QImage &image, const QColor &color)
Tests whether an image is consists only of pixels matching the specified color.
static QRect nonTransparentImageRect(const QImage &image, QSize minSize=QSize(), bool center=false)
Calculates the non-transparent region of an image.
static void stackBlur(QImage &image, int radius, bool alphaOnly=false, QgsFeedback *feedback=nullptr)
Performs a stack blur on an image.
static QImage cropTransparent(const QImage &image, QSize minSize=QSize(), bool center=false)
Crop any transparent border from around an image.
static void convertToGrayscale(QImage &image, GrayscaleMode mode=GrayscaleLuminosity, QgsFeedback *feedback=nullptr)
Convert a QImage to a grayscale image.
GrayscaleMode
Modes for converting a QImage to grayscale.
@ GrayscaleLightness
Keep the lightness of the color, drops the saturation.
@ GrayscaleLuminosity
Grayscale by perceptual luminosity (weighted sum of color RGB components).
@ GrayscaleAverage
Grayscale by taking average of color RGB components.
QString qgsEnumValueToKey(const T &value, bool *returnOk=nullptr)
Returns the value for the given key of an enum.
Definition qgis.h:7831
unsigned long long qgssize
Qgssize is used instead of size_t, because size_t is stdlib type, unknown by SIP, and it would be har...
Definition qgis.h:8174
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference).
Definition qgis.h:7557
#define INF
#define BLOCK_THREADS
bool colorsMatchFloodFill(QRgb c1, QRgb c2, int tolerance)
#define QgsDebugError(str)
Definition qgslogger.h:71
Struct for storing properties of a distance transform operation.
bool useMaxDistance
Set to true to automatically calculate the maximum distance in the transform to use as the spread val...
bool shadeExterior
Set to true to perform the distance transform on transparent pixels in the source image,...
double spread
Maximum distance (in pixels) for the distance transform shading to spread.
QgsColorRamp * ramp
Color ramp to use for shading the distance transform.