QGIS API Documentation 3.37.0-Master (fdefdf9c27f)
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#include "qgis.h"
20#include "qgscolorramp.h"
21#include "qgslogger.h"
22#include "qgsfeedback.h"
23#include <QtConcurrentMap>
24#include <QColor>
25#include <QPainter>
26
27//determined via trial-and-error. Could possibly be optimised, or varied
28//depending on the image size.
29#define BLOCK_THREADS 16
30
31#define INF 1E20
32
34
35template <typename PixelOperation>
36void QgsImageOperation::runPixelOperation( QImage &image, PixelOperation &operation, QgsFeedback *feedback )
37{
38 if ( static_cast< qgssize >( image.height() ) * image.width() < 100000 )
39 {
40 //small image, don't multithread
41 //this threshold was determined via testing various images
42 runPixelOperationOnWholeImage( image, operation, feedback );
43 }
44 else
45 {
46 //large image, multithread operation
47 QgsImageOperation::ProcessBlockUsingPixelOperation<PixelOperation> blockOp( operation, feedback );
48 runBlockOperationInThreads( image, blockOp, QgsImageOperation::ByRow );
49 }
50}
51
52template <typename PixelOperation>
53void QgsImageOperation::runPixelOperationOnWholeImage( QImage &image, PixelOperation &operation, QgsFeedback *feedback )
54{
55 int height = image.height();
56 int width = image.width();
57 for ( int y = 0; y < height; ++y )
58 {
59 if ( feedback && feedback->isCanceled() )
60 break;
61
62 QRgb *ref = reinterpret_cast< QRgb * >( image.scanLine( y ) );
63 for ( int x = 0; x < width; ++x )
64 {
65 operation( ref[x], x, y );
66 }
67 }
68}
69
70//rect operations
71
72template <typename RectOperation>
73void QgsImageOperation::runRectOperation( QImage &image, RectOperation &operation )
74{
75 //possibly could be tweaked for rect operations
76 if ( static_cast< qgssize >( image.height() ) * image.width() < 100000 )
77 {
78 //small image, don't multithread
79 //this threshold was determined via testing various images
80 runRectOperationOnWholeImage( image, operation );
81 }
82 else
83 {
84 //large image, multithread operation
85 runBlockOperationInThreads( image, operation, ByRow );
86 }
87}
88
89template <class RectOperation>
90void QgsImageOperation::runRectOperationOnWholeImage( QImage &image, RectOperation &operation )
91{
92 ImageBlock fullImage;
93 fullImage.beginLine = 0;
94 fullImage.endLine = image.height();
95 fullImage.lineLength = image.width();
96 fullImage.image = &image;
97
98 operation( fullImage );
99}
100
101//linear operations
102
103template <typename LineOperation>
104void QgsImageOperation::runLineOperation( QImage &image, LineOperation &operation, QgsFeedback *feedback )
105{
106 //possibly could be tweaked for rect operations
107 if ( static_cast< qgssize >( image.height() ) * image.width() < 100000 )
108 {
109 //small image, don't multithread
110 //this threshold was determined via testing various images
111 runLineOperationOnWholeImage( image, operation, feedback );
112 }
113 else
114 {
115 //large image, multithread operation
116 QgsImageOperation::ProcessBlockUsingLineOperation<LineOperation> blockOp( operation );
117 runBlockOperationInThreads( image, blockOp, operation.direction() );
118 }
119}
120
121template <class LineOperation>
122void QgsImageOperation::runLineOperationOnWholeImage( QImage &image, LineOperation &operation, QgsFeedback *feedback )
123{
124 int height = image.height();
125 int width = image.width();
126
127 //do something with whole lines
128 int bpl = image.bytesPerLine();
129 if ( operation.direction() == ByRow )
130 {
131 for ( int y = 0; y < height; ++y )
132 {
133 if ( feedback && feedback->isCanceled() )
134 break;
135
136 QRgb *ref = reinterpret_cast< QRgb * >( image.scanLine( y ) );
137 operation( ref, width, bpl );
138 }
139 }
140 else
141 {
142 //by column
143 unsigned char *ref = image.scanLine( 0 );
144 for ( int x = 0; x < width; ++x, ref += 4 )
145 {
146 if ( feedback && feedback->isCanceled() )
147 break;
148
149 operation( reinterpret_cast< QRgb * >( ref ), height, bpl );
150 }
151 }
152}
153
154
155//multithreaded block processing
156
157template <typename BlockOperation>
158void 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;
215 case GrayscaleLuminosity:
216 grayscaleLuminosityOp( rgb );
217 return;
218 case GrayscaleAverage:
219 grayscaleAverageOp( rgb );
220 return;
221 case GrayscaleLightness:
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 )
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,
284 colorizeColor.hue(), colorizeColor.saturation(), colorizeStrength );
285 runPixelOperation( image, operation, feedback );
286}
287
288void QgsImageOperation::HueSaturationPixelOperation::operator()( QRgb &rgb, const int x, const int y )
289{
290 Q_UNUSED( x )
291 Q_UNUSED( y )
292 QColor tmpColor( rgb );
293 int h, s, l;
294 tmpColor.getHsl( &h, &s, &l );
295
296 if ( mSaturation < 1.0 )
297 {
298 // Lowering the saturation. Use a simple linear relationship
299 s = std::min( static_cast< int >( s * mSaturation ), 255 );
300 }
301 else if ( mSaturation > 1.0 )
302 {
303 // Raising the saturation. Use a saturation curve to prevent
304 // clipping at maximum saturation with ugly results.
305 s = std::min( static_cast< int >( 255. * ( 1 - std::pow( 1 - ( s / 255. ), std::pow( mSaturation, 2 ) ) ) ), 255 );
306 }
307
308 if ( mColorize )
309 {
310 h = mColorizeHue;
311 s = mColorizeSaturation;
312 if ( mColorizeStrength < 1.0 )
313 {
314 //get rgb for colorized color
315 QColor colorizedColor = QColor::fromHsl( h, s, l );
316 int colorizedR, colorizedG, colorizedB;
317 colorizedColor.getRgb( &colorizedR, &colorizedG, &colorizedB );
318
319 // Now, linearly scale by colorize strength
320 int r = mColorizeStrength * colorizedR + ( 1 - mColorizeStrength ) * tmpColor.red();
321 int g = mColorizeStrength * colorizedG + ( 1 - mColorizeStrength ) * tmpColor.green();
322 int b = mColorizeStrength * colorizedB + ( 1 - mColorizeStrength ) * tmpColor.blue();
323
324 rgb = qRgba( r, g, b, qAlpha( rgb ) );
325 return;
326 }
327 }
328
329 tmpColor.setHsl( h, s, l, qAlpha( rgb ) );
330 rgb = tmpColor.rgba();
331}
332
333//multiply opacity
334
335void QgsImageOperation::multiplyOpacity( QImage &image, const double factor, QgsFeedback *feedback )
336{
337 if ( qgsDoubleNear( factor, 1.0 ) )
338 {
339 //no change
340 return;
341 }
342 else if ( factor < 1.0 )
343 {
344 //decreasing opacity - we can use the faster DestinationIn composition mode
345 //to reduce the alpha channel
346 QColor transparentFillColor = QColor( 0, 0, 0, 255 * factor );
347 image.detach();
348 QPainter painter( &image );
349 painter.setCompositionMode( QPainter::CompositionMode_DestinationIn );
350 painter.fillRect( 0, 0, image.width(), image.height(), transparentFillColor );
351 painter.end();
352 }
353 else
354 {
355 //increasing opacity - run this as a pixel operation for multithreading
356 image.detach();
357 MultiplyOpacityPixelOperation operation( factor );
358 runPixelOperation( image, operation, feedback );
359 }
360}
361
362void QgsImageOperation::MultiplyOpacityPixelOperation::operator()( QRgb &rgb, const int x, const int y )
363{
364 Q_UNUSED( x )
365 Q_UNUSED( y )
366 rgb = qRgba( qRed( rgb ), qGreen( rgb ), qBlue( rgb ), std::clamp( std::round( mFactor * qAlpha( rgb ) ), 0.0, 255.0 ) );
367}
368
369// overlay color
370
371void QgsImageOperation::overlayColor( QImage &image, const QColor &color )
372{
373 QColor opaqueColor = color;
374 opaqueColor.setAlpha( 255 );
375
376 //use QPainter SourceIn composition mode to overlay color (fast)
377 //this retains image's alpha channel but replaces color
378 image.detach();
379 QPainter painter( &image );
380 painter.setCompositionMode( QPainter::CompositionMode_SourceIn );
381 painter.fillRect( 0, 0, image.width(), image.height(), opaqueColor );
382 painter.end();
383}
384
385// distance transform
386
387void QgsImageOperation::distanceTransform( QImage &image, const DistanceTransformProperties &properties, QgsFeedback *feedback )
388{
389 if ( ! properties.ramp )
390 {
391 QgsDebugError( QStringLiteral( "no color ramp specified for distance transform" ) );
392 return;
393 }
394
395 //first convert to 1 bit alpha mask array
396 std::unique_ptr<double[]> array( new double[ static_cast< qgssize >( image.width() ) * image.height()] );
397 if ( feedback && feedback->isCanceled() )
398 return;
399
400 image.detach();
401 ConvertToArrayPixelOperation convertToArray( image.width(), array.get(), properties.shadeExterior );
402 runPixelOperation( image, convertToArray, feedback );
403 if ( feedback && feedback->isCanceled() )
404 return;
405
406 //calculate distance transform (single threaded only)
407 distanceTransform2d( array.get(), image.width(), image.height(), feedback );
408 if ( feedback && feedback->isCanceled() )
409 return;
410
411 double spread;
412 if ( properties.useMaxDistance )
413 {
414 spread = std::sqrt( maxValueInDistanceTransformArray( array.get(), image.width() * image.height() ) );
415 }
416 else
417 {
418 spread = properties.spread;
419 }
420
421 if ( feedback && feedback->isCanceled() )
422 return;
423
424 //shade distance transform
425 ShadeFromArrayOperation shadeFromArray( image.width(), array.get(), spread, properties );
426 runPixelOperation( image, shadeFromArray, feedback );
427}
428
429void QgsImageOperation::ConvertToArrayPixelOperation::operator()( QRgb &rgb, const int x, const int y )
430{
431 qgssize idx = y * static_cast< qgssize >( mWidth ) + x;
432 if ( mExterior )
433 {
434 if ( qAlpha( rgb ) > 0 )
435 {
436 //opaque pixel, so zero distance
437 mArray[ idx ] = 1 - qAlpha( rgb ) / 255.0;
438 }
439 else
440 {
441 //transparent pixel, so initially set distance as infinite
442 mArray[ idx ] = INF;
443 }
444 }
445 else
446 {
447 //TODO - fix this for semi-transparent pixels
448 if ( qAlpha( rgb ) == 255 )
449 {
450 mArray[ idx ] = INF;
451 }
452 else
453 {
454 mArray[idx] = 0;
455 }
456 }
457}
458
459//fast distance transform code, adapted from http://cs.brown.edu/~pff/dt/
460
461/* distance transform of a 1d function using squared distance */
462void QgsImageOperation::distanceTransform1d( double *f, int n, int *v, double *z, double *d )
463{
464 int k = 0;
465 v[0] = 0;
466 z[0] = -INF;
467 z[1] = + INF;
468 for ( int q = 1; q <= n - 1; q++ )
469 {
470 double s = ( ( f[q] + q * q ) - ( f[v[k]] + ( v[k] * v[k] ) ) ) / ( 2 * q - 2 * v[k] );
471 while ( s <= z[k] )
472 {
473 k--;
474 s = ( ( f[q] + q * q ) - ( f[v[k]] + ( v[k] * v[k] ) ) ) / ( 2 * q - 2 * v[k] );
475 }
476 k++;
477 v[k] = q;
478 z[k] = s;
479 z[k + 1] = + INF;
480 }
481
482 k = 0;
483 for ( int q = 0; q <= n - 1; q++ )
484 {
485 while ( z[k + 1] < q )
486 k++;
487 d[q] = ( q - v[k] ) * ( q - v[k] ) + f[v[k]];
488 }
489}
490
491double QgsImageOperation::maxValueInDistanceTransformArray( const double *array, const unsigned int size )
492{
493 double dtMaxValue = array[0];
494 for ( unsigned int i = 1; i < size; ++i )
495 {
496 if ( array[i] > dtMaxValue )
497 {
498 dtMaxValue = array[i];
499 }
500 }
501 return dtMaxValue;
502}
503
504/* distance transform of 2d function using squared distance */
505void QgsImageOperation::distanceTransform2d( double *im, int width, int height, QgsFeedback *feedback )
506{
507 int maxDimension = std::max( width, height );
508
509 std::unique_ptr<double[]> f( new double[ maxDimension ] );
510 std::unique_ptr<int []> v( new int[ maxDimension ] );
511 std::unique_ptr<double[]>z( new double[ maxDimension + 1 ] );
512 std::unique_ptr<double[]>d( new double[ maxDimension ] );
513
514 // transform along columns
515 for ( int x = 0; x < width; x++ )
516 {
517 if ( feedback && feedback->isCanceled() )
518 break;
519
520 for ( int y = 0; y < height; y++ )
521 {
522 f[y] = im[ x + y * width ];
523 }
524 distanceTransform1d( f.get(), height, v.get(), z.get(), d.get() );
525 for ( int y = 0; y < height; y++ )
526 {
527 im[ x + y * width ] = d[y];
528 }
529 }
530
531 // transform along rows
532 for ( int y = 0; y < height; y++ )
533 {
534 if ( feedback && feedback->isCanceled() )
535 break;
536
537 for ( int x = 0; x < width; x++ )
538 {
539 f[x] = im[ x + y * width ];
540 }
541 distanceTransform1d( f.get(), width, v.get(), z.get(), d.get() );
542 for ( int x = 0; x < width; x++ )
543 {
544 im[ x + y * width ] = d[x];
545 }
546 }
547}
548
549void QgsImageOperation::ShadeFromArrayOperation::operator()( QRgb &rgb, const int x, const int y )
550{
551 if ( ! mProperties.ramp )
552 return;
553
554 if ( qgsDoubleNear( mSpread, 0.0 ) )
555 {
556 rgb = mProperties.ramp->color( 1.0 ).rgba();
557 return;
558 }
559
560 int idx = y * mWidth + x;
561
562 //values are distance squared
563 double squaredVal = mArray[ idx ];
564 if ( squaredVal > mSpreadSquared )
565 {
566 rgb = Qt::transparent;
567 return;
568 }
569
570 double distance = std::sqrt( squaredVal );
571 double val = distance / mSpread;
572 QColor rampColor = mProperties.ramp->color( val );
573
574 if ( ( mProperties.shadeExterior && distance > mSpread - 1 ) )
575 {
576 //fade off final pixel to antialias edge
577 double alphaMultiplyFactor = mSpread - distance;
578 rampColor.setAlpha( rampColor.alpha() * alphaMultiplyFactor );
579 }
580 rgb = rampColor.rgba();
581}
582
583//stack blur
584
585void QgsImageOperation::stackBlur( QImage &image, const int radius, const bool alphaOnly, QgsFeedback *feedback )
586{
587 // culled from Qt's qpixmapfilter.cpp, see: http://www.qtcentre.org/archive/index.php/t-26534.html
588 int tab[] = { 14, 10, 8, 6, 5, 5, 4, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2 };
589 int alpha = ( radius < 1 ) ? 16 : ( radius > 17 ) ? 1 : tab[radius - 1];
590
591 int i1 = 0;
592 int i2 = 3;
593
594 //ensure correct source format.
595 QImage::Format originalFormat = image.format();
596 QImage *pImage = &image;
597 std::unique_ptr< QImage> convertedImage;
598 if ( !alphaOnly && originalFormat != QImage::Format_ARGB32_Premultiplied )
599 {
600 convertedImage = std::make_unique< QImage >( image.convertToFormat( QImage::Format_ARGB32_Premultiplied ) );
601 pImage = convertedImage.get();
602 }
603 else if ( alphaOnly && originalFormat != QImage::Format_ARGB32 )
604 {
605 convertedImage = std::make_unique< QImage >( image.convertToFormat( QImage::Format_ARGB32 ) );
606 pImage = convertedImage.get();
607 }
608 else
609 {
610 image.detach();
611 }
612
613 if ( feedback && feedback->isCanceled() )
614 return;
615
616 if ( alphaOnly )
617 i1 = i2 = ( QSysInfo::ByteOrder == QSysInfo::BigEndian ? 0 : 3 );
618
619 StackBlurLineOperation topToBottomBlur( alpha, QgsImageOperation::ByColumn, true, i1, i2, feedback );
620 runLineOperation( *pImage, topToBottomBlur, feedback );
621
622 if ( feedback && feedback->isCanceled() )
623 return;
624
625 StackBlurLineOperation leftToRightBlur( alpha, QgsImageOperation::ByRow, true, i1, i2, feedback );
626 runLineOperation( *pImage, leftToRightBlur, feedback );
627
628 if ( feedback && feedback->isCanceled() )
629 return;
630
631 StackBlurLineOperation bottomToTopBlur( alpha, QgsImageOperation::ByColumn, false, i1, i2, feedback );
632 runLineOperation( *pImage, bottomToTopBlur, feedback );
633
634 if ( feedback && feedback->isCanceled() )
635 return;
636
637 StackBlurLineOperation rightToLeftBlur( alpha, QgsImageOperation::ByRow, false, i1, i2, feedback );
638 runLineOperation( *pImage, rightToLeftBlur, feedback );
639
640 if ( feedback && feedback->isCanceled() )
641 return;
642
643 if ( pImage->format() != originalFormat )
644 {
645 image = pImage->convertToFormat( originalFormat );
646 }
647}
648
649//gaussian blur
650
651QImage *QgsImageOperation::gaussianBlur( QImage &image, const int radius, QgsFeedback *feedback )
652{
653 int width = image.width();
654 int height = image.height();
655
656 if ( radius <= 0 )
657 {
658 //just make an unchanged copy
659 QImage *copy = new QImage( image.copy() );
660 return copy;
661 }
662
663 std::unique_ptr<double[]>kernel( createGaussianKernel( radius ) );
664 if ( feedback && feedback->isCanceled() )
665 return new QImage();
666
667 //ensure correct source format.
668 QImage::Format originalFormat = image.format();
669 QImage *pImage = &image;
670 std::unique_ptr< QImage> convertedImage;
671 if ( originalFormat != QImage::Format_ARGB32_Premultiplied )
672 {
673 convertedImage = std::make_unique< QImage >( image.convertToFormat( QImage::Format_ARGB32_Premultiplied ) );
674 pImage = convertedImage.get();
675 }
676 else
677 {
678 image.detach();
679 }
680 if ( feedback && feedback->isCanceled() )
681 return new QImage();
682
683 //blur along rows
684 QImage xBlurImage = QImage( width, height, QImage::Format_ARGB32_Premultiplied );
685 GaussianBlurOperation rowBlur( radius, QgsImageOperation::ByRow, &xBlurImage, kernel.get(), feedback );
686 runRectOperation( *pImage, rowBlur );
687
688 if ( feedback && feedback->isCanceled() )
689 return new QImage();
690
691 //blur along columns
692 std::unique_ptr< QImage > yBlurImage = std::make_unique< QImage >( width, height, QImage::Format_ARGB32_Premultiplied );
693 GaussianBlurOperation colBlur( radius, QgsImageOperation::ByColumn, yBlurImage.get(), kernel.get(), feedback );
694 runRectOperation( xBlurImage, colBlur );
695
696 if ( feedback && feedback->isCanceled() )
697 return new QImage();
698
699 kernel.reset();
700
701 if ( originalFormat != QImage::Format_ARGB32_Premultiplied )
702 {
703 return new QImage( yBlurImage->convertToFormat( originalFormat ) );
704 }
705
706 return yBlurImage.release();
707}
708
709void QgsImageOperation::GaussianBlurOperation::operator()( QgsImageOperation::ImageBlock &block )
710{
711 if ( mFeedback && mFeedback->isCanceled() )
712 return;
713
714 int width = block.image->width();
715 int height = block.image->height();
716 int sourceBpl = block.image->bytesPerLine();
717
718 unsigned char *outputLineRef = mDestImage->scanLine( block.beginLine );
719 QRgb *destRef = nullptr;
720 if ( mDirection == ByRow )
721 {
722 unsigned char *sourceFirstLine = block.image->scanLine( 0 );
723 unsigned char *sourceRef;
724
725 //blur along rows
726 for ( unsigned int y = block.beginLine; y < block.endLine; ++y, outputLineRef += mDestImageBpl )
727 {
728 if ( mFeedback && mFeedback->isCanceled() )
729 break;
730
731 sourceRef = sourceFirstLine;
732 destRef = reinterpret_cast< QRgb * >( outputLineRef );
733 for ( int x = 0; x < width; ++x, ++destRef, sourceRef += 4 )
734 {
735 if ( mFeedback && mFeedback->isCanceled() )
736 break;
737
738 *destRef = gaussianBlurVertical( y, sourceRef, sourceBpl, height );
739 }
740 }
741 }
742 else
743 {
744 unsigned char *sourceRef = block.image->scanLine( block.beginLine );
745 for ( unsigned int y = block.beginLine; y < block.endLine; ++y, outputLineRef += mDestImageBpl, sourceRef += sourceBpl )
746 {
747 if ( mFeedback && mFeedback->isCanceled() )
748 break;
749
750 destRef = reinterpret_cast< QRgb * >( outputLineRef );
751 for ( int x = 0; x < width; ++x, ++destRef )
752 {
753 if ( mFeedback && mFeedback->isCanceled() )
754 break;
755
756 *destRef = gaussianBlurHorizontal( x, sourceRef, width );
757 }
758 }
759 }
760}
761
762inline QRgb QgsImageOperation::GaussianBlurOperation::gaussianBlurVertical( const int posy, unsigned char *sourceFirstLine, const int sourceBpl, const int height ) const
763{
764 double r = 0;
765 double b = 0;
766 double g = 0;
767 double a = 0;
768 int y;
769 unsigned char *ref;
770
771 for ( int i = 0; i <= mRadius * 2; ++i )
772 {
773 y = std::clamp( posy + ( i - mRadius ), 0, height - 1 );
774 ref = sourceFirstLine + static_cast< std::size_t >( sourceBpl ) * y;
775
776 QRgb *refRgb = reinterpret_cast< QRgb * >( ref );
777 r += mKernel[i] * qRed( *refRgb );
778 g += mKernel[i] * qGreen( *refRgb );
779 b += mKernel[i] * qBlue( *refRgb );
780 a += mKernel[i] * qAlpha( *refRgb );
781 }
782
783 return qRgba( r, g, b, a );
784}
785
786inline QRgb QgsImageOperation::GaussianBlurOperation::gaussianBlurHorizontal( const int posx, unsigned char *sourceFirstLine, const int width ) const
787{
788 double r = 0;
789 double b = 0;
790 double g = 0;
791 double a = 0;
792 int x;
793 unsigned char *ref;
794
795 for ( int i = 0; i <= mRadius * 2; ++i )
796 {
797 x = std::clamp( posx + ( i - mRadius ), 0, width - 1 );
798 ref = sourceFirstLine + x * 4;
799
800 QRgb *refRgb = reinterpret_cast< QRgb * >( ref );
801 r += mKernel[i] * qRed( *refRgb );
802 g += mKernel[i] * qGreen( *refRgb );
803 b += mKernel[i] * qBlue( *refRgb );
804 a += mKernel[i] * qAlpha( *refRgb );
805 }
806
807 return qRgba( r, g, b, a );
808}
809
810
811double *QgsImageOperation::createGaussianKernel( const int radius )
812{
813 double *kernel = new double[ radius * 2 + 1 ];
814 double sigma = radius / 3.0;
815 double twoSigmaSquared = 2 * sigma * sigma;
816 double coefficient = 1.0 / std::sqrt( M_PI * twoSigmaSquared );
817 double expCoefficient = -1.0 / twoSigmaSquared;
818
819 double sum = 0;
820 double result;
821 for ( int i = 0; i <= radius; ++i )
822 {
823 result = coefficient * std::exp( i * i * expCoefficient );
824 kernel[ radius - i ] = result;
825 sum += result;
826 if ( i > 0 )
827 {
828 kernel[radius + i] = result;
829 sum += result;
830 }
831 }
832 //normalize
833 for ( int i = 0; i <= radius * 2; ++i )
834 {
835 kernel[i] /= sum;
836 }
837 return kernel;
838}
839
840
841// flip
842
844{
845 image.detach();
846 FlipLineOperation flipOperation( type == QgsImageOperation::FlipHorizontal ? QgsImageOperation::ByRow : QgsImageOperation::ByColumn );
847 runLineOperation( image, flipOperation );
848}
849
850QRect QgsImageOperation::nonTransparentImageRect( const QImage &image, QSize minSize, bool center )
851{
852 int width = image.width();
853 int height = image.height();
854 int xmin = width;
855 int xmax = 0;
856 int ymin = height;
857 int ymax = 0;
858
859 // scan down till we hit something
860 for ( int y = 0; y < height; ++y )
861 {
862 bool found = false;
863 const QRgb *imgScanline = reinterpret_cast< const QRgb * >( image.constScanLine( y ) );
864 for ( int x = 0; x < width; ++x )
865 {
866 if ( qAlpha( imgScanline[x] ) )
867 {
868 ymin = y;
869 ymax = y;
870 xmin = x;
871 xmax = x;
872 found = true;
873 break;
874 }
875 }
876 if ( found )
877 break;
878 }
879
880 //scan up till we hit something
881 for ( int y = height - 1; y >= ymin; --y )
882 {
883 bool found = false;
884 const QRgb *imgScanline = reinterpret_cast< const QRgb * >( image.constScanLine( y ) );
885 for ( int x = 0; x < width; ++x )
886 {
887 if ( qAlpha( imgScanline[x] ) )
888 {
889 ymax = y;
890 xmin = std::min( xmin, x );
891 xmax = std::max( xmax, x );
892 found = true;
893 break;
894 }
895 }
896 if ( found )
897 break;
898 }
899
900 //scan left to right till we hit something, using a refined y region
901 for ( int y = ymin; y <= ymax; ++y )
902 {
903 const QRgb *imgScanline = reinterpret_cast< const QRgb * >( image.constScanLine( y ) );
904 for ( int x = 0; x < xmin; ++x )
905 {
906 if ( qAlpha( imgScanline[x] ) )
907 {
908 xmin = x;
909 break;
910 }
911 }
912 }
913
914 //scan right to left till we hit something, using the refined y region
915 for ( int y = ymin; y <= ymax; ++y )
916 {
917 const QRgb *imgScanline = reinterpret_cast< const QRgb * >( image.constScanLine( y ) );
918 for ( int x = width - 1; x > xmax; --x )
919 {
920 if ( qAlpha( imgScanline[x] ) )
921 {
922 xmax = x;
923 break;
924 }
925 }
926 }
927
928 if ( minSize.isValid() )
929 {
930 if ( xmax - xmin < minSize.width() ) // centers image on x
931 {
932 xmin = std::max( ( xmax + xmin ) / 2 - minSize.width() / 2, 0 );
933 xmax = xmin + minSize.width();
934 }
935 if ( ymax - ymin < minSize.height() ) // centers image on y
936 {
937 ymin = std::max( ( ymax + ymin ) / 2 - minSize.height() / 2, 0 );
938 ymax = ymin + minSize.height();
939 }
940 }
941 if ( center )
942 {
943 // recompute min and max to center image
944 const int dx = std::max( std::abs( xmax - width / 2 ), std::abs( xmin - width / 2 ) );
945 const int dy = std::max( std::abs( ymax - height / 2 ), std::abs( ymin - height / 2 ) );
946 xmin = std::max( 0, width / 2 - dx );
947 xmax = std::min( width, width / 2 + dx );
948 ymin = std::max( 0, height / 2 - dy );
949 ymax = std::min( height, height / 2 + dy );
950 }
951
952 return QRect( xmin, ymin, xmax - xmin, ymax - ymin );
953}
954
955QImage QgsImageOperation::cropTransparent( const QImage &image, QSize minSize, bool center )
956{
957 return image.copy( QgsImageOperation::nonTransparentImageRect( image, minSize, center ) );
958}
959
960void QgsImageOperation::FlipLineOperation::operator()( QRgb *startRef, const int lineLength, const int bytesPerLine ) const
961{
962 int increment = ( mDirection == QgsImageOperation::ByRow ) ? 4 : bytesPerLine;
963
964 //store temporary line
965 unsigned char *p = reinterpret_cast< unsigned char * >( startRef );
966 unsigned char *tempLine = new unsigned char[ lineLength * 4 ];
967 for ( int i = 0; i < lineLength * 4; ++i, p += increment )
968 {
969 tempLine[i++] = *( p++ );
970 tempLine[i++] = *( p++ );
971 tempLine[i++] = *( p++ );
972 tempLine[i] = *( p );
973 p -= 3;
974 }
975
976 //write values back in reverse order
977 p = reinterpret_cast< unsigned char * >( startRef );
978 for ( int i = ( lineLength - 1 ) * 4; i >= 0; i -= 7, p += increment )
979 {
980 *( p++ ) = tempLine[i++];
981 *( p++ ) = tempLine[i++];
982 *( p++ ) = tempLine[i++];
983 *( p ) = tempLine[i];
984 p -= 3;
985 }
986
987 delete[] tempLine;
988}
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:53
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 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.
@ GrayscaleOff
No change.
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:5747
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition: qgis.h:5207
#define INF
#define BLOCK_THREADS
#define QgsDebugError(str)
Definition: qgslogger.h:38
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.