QGIS API Documentation  3.25.0-Master (6b426f5f8a)
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 : [email protected]
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 
35 template <typename PixelOperation>
36 void 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 
52 template <typename PixelOperation>
53 void 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 
72 template <typename RectOperation>
73 void 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 
89 template <class RectOperation>
90 void 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 
103 template <typename LineOperation>
104 void 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 
121 template <class LineOperation>
122 void 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 
157 template <typename BlockOperation>
158 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 
195 void 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 
207 void 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 
228 void 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 
241 void 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 
247 void 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 
256 void 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 
263 void 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 
273 int 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 
280 void 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 
288 void 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 
335 void 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 
362 void 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 
371 void 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 
387 void QgsImageOperation::distanceTransform( QImage &image, const DistanceTransformProperties &properties, QgsFeedback *feedback )
388 {
389  if ( ! properties.ramp )
390  {
391  QgsDebugMsg( 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 
429 void 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 */
462 void 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 
491 double 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 */
505 void 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 
549 void 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 
585 void 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 
651 QImage *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 
709 void 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 
762 inline 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 + 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 
786 inline 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 
811 double *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 
850 QRect 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 
955 QImage QgsImageOperation::cropTransparent( const QImage &image, QSize minSize, bool center )
956 {
957  return image.copy( QgsImageOperation::nonTransparentImageRect( image, minSize, center ) );
958 }
959 
960 void 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:45
bool isCanceled() const SIP_HOLDGIL
Tells whether the operation has been canceled already.
Definition: qgsfeedback.h:54
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:2504
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition: qgis.h:1978
#define INF
#define BLOCK_THREADS
#define QgsDebugMsg(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.