QGIS API Documentation 4.1.0-Master (3b8ef1f72a3)
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
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
963inline bool colorsMatchFloodFill( QRgb c1, QRgb c2, int tolerance )
964{
965 if ( tolerance == 0 )
966 {
967 return c1 == c2;
968 }
969 return std::abs( qRed( c1 ) - qRed( c2 ) ) <= tolerance
970 && std::abs( qGreen( c1 ) - qGreen( c2 ) ) <= tolerance
971 && std::abs( qBlue( c1 ) - qBlue( c2 ) ) <= tolerance
972 && std::abs( qAlpha( c1 ) - qAlpha( c2 ) ) <= tolerance;
973}
974
975QImage QgsImageOperation::floodFill( const QImage &image, const QPoint &startPoint, const QColor &newColor, int tolerance, QgsFeedback *feedback )
976{
977 if ( image.isNull() || !image.rect().contains( startPoint ) )
978 {
979 return image;
980 }
981
982 QImage resultImage;
983 if ( image.format() != QImage::Format_ARGB32 && image.format() != QImage::Format_ARGB32_Premultiplied && image.format() != QImage::Format_RGB32 )
984 {
985 resultImage = image.convertToFormat( QImage::Format_ARGB32 );
986 }
987 else
988 {
989 resultImage = image.copy();
990 }
991
992 const QRgb targetColorRgb = resultImage.pixel( startPoint );
993 const QRgb newColorRgb = newColor.rgba();
994 if ( colorsMatchFloodFill( targetColorRgb, newColorRgb, tolerance ) )
995 {
996 return resultImage;
997 }
998
999 const int width = resultImage.width();
1000 const int height = resultImage.height();
1001
1002 std::stack<QPoint> stack;
1003 stack.push( startPoint );
1004 while ( !stack.empty() )
1005 {
1006 if ( feedback && feedback->isCanceled() )
1007 {
1008 break;
1009 }
1010
1011 const QPoint pt = stack.top();
1012 stack.pop();
1013 const int x = pt.x();
1014 const int y = pt.y();
1015 QRgb *scanline = reinterpret_cast< QRgb * >( resultImage.scanLine( y ) );
1016 int x1 = x;
1017 while ( x1 >= 0 && colorsMatchFloodFill( scanline[x1], targetColorRgb, tolerance ) )
1018 {
1019 x1--;
1020 }
1021 x1++;
1022 int x2 = x;
1023 while ( x2 < width && colorsMatchFloodFill( scanline[x2], targetColorRgb, tolerance ) )
1024 {
1025 scanline[x2] = newColorRgb;
1026 x2++;
1027 }
1028 x2--;
1029
1030 bool spanAbove = false;
1031 bool spanBelow = false;
1032
1033 QRgb *scanlineAbove = ( y > 0 ) ? reinterpret_cast< QRgb * >( resultImage.scanLine( y - 1 ) ) : nullptr;
1034 QRgb *scanlineBelow = ( y < height - 1 ) ? reinterpret_cast< QRgb * >( resultImage.scanLine( y + 1 ) ) : nullptr;
1035 for ( int currX = x1; currX <= x2; currX++ )
1036 {
1037 // check the row above
1038 if ( scanlineAbove )
1039 {
1040 const bool match = colorsMatchFloodFill( scanlineAbove[currX], targetColorRgb, tolerance );
1041 if ( !spanAbove && match )
1042 {
1043 stack.push( QPoint( currX, y - 1 ) );
1044 spanAbove = true;
1045 }
1046 else if ( spanAbove && !match )
1047 {
1048 spanAbove = false;
1049 }
1050 }
1051
1052 // check the row below
1053 if ( scanlineBelow )
1054 {
1055 const bool match = colorsMatchFloodFill( scanlineBelow[currX], targetColorRgb, tolerance );
1056 if ( !spanBelow && match )
1057 {
1058 stack.push( QPoint( currX, y + 1 ) );
1059 spanBelow = true;
1060 }
1061 else if ( spanBelow && !match )
1062 {
1063 spanBelow = false;
1064 }
1065 }
1066 }
1067 }
1068
1069 return resultImage;
1070}
1071
1072void QgsImageOperation::FlipLineOperation::operator()( QRgb *startRef, const int lineLength, const int bytesPerLine ) const
1073{
1074 int increment = ( mDirection == QgsImageOperation::ByRow ) ? 4 : bytesPerLine;
1075
1076 //store temporary line
1077 unsigned char *p = reinterpret_cast< unsigned char * >( startRef );
1078 unsigned char *tempLine = new unsigned char[lineLength * 4];
1079 for ( int i = 0; i < lineLength * 4; ++i, p += increment )
1080 {
1081 tempLine[i++] = *( p++ );
1082 tempLine[i++] = *( p++ );
1083 tempLine[i++] = *( p++ );
1084 tempLine[i] = *( p );
1085 p -= 3;
1086 }
1087
1088 //write values back in reverse order
1089 p = reinterpret_cast< unsigned char * >( startRef );
1090 for ( int i = ( lineLength - 1 ) * 4; i >= 0; i -= 7, p += increment )
1091 {
1092 *( p++ ) = tempLine[i++];
1093 *( p++ ) = tempLine[i++];
1094 *( p++ ) = tempLine[i++];
1095 *( p ) = tempLine[i];
1096 p -= 3;
1097 }
1098
1099 delete[] tempLine;
1100}
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 QImage * gaussianBlur(QImage &image, int radius, QgsFeedback *feedback=nullptr)
Performs a gaussian blur on an image.
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.
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:7602
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference).
Definition qgis.h:7077
#define INF
#define BLOCK_THREADS
bool colorsMatchFloodFill(QRgb c1, QRgb c2, int tolerance)
#define QgsDebugError(str)
Definition qgslogger.h:59
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.