QGIS API Documentation  3.26.3-Buenos Aires (65e4edfdad)
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 }
QgsImageOperation::DistanceTransformProperties::ramp
QgsColorRamp * ramp
Color ramp to use for shading the distance transform.
Definition: qgsimageoperation.h:147
QgsFeedback::isCanceled
bool isCanceled() const SIP_HOLDGIL
Tells whether the operation has been canceled already.
Definition: qgsfeedback.h:67
qgis.h
QgsImageOperation::DistanceTransformProperties::useMaxDistance
bool useMaxDistance
Set to true to automatically calculate the maximum distance in the transform to use as the spread val...
Definition: qgsimageoperation.h:136
QgsImageOperation::GrayscaleOff
@ GrayscaleOff
No change.
Definition: qgsimageoperation.h:60
qgsimageoperation.h
QgsDebugMsg
#define QgsDebugMsg(str)
Definition: qgslogger.h:38
QgsImageOperation::cropTransparent
static QImage cropTransparent(const QImage &image, QSize minSize=QSize(), bool center=false)
Crop any transparent border from around an image.
Definition: qgsimageoperation.cpp:955
QgsImageOperation::FlipType
FlipType
Flip operation types.
Definition: qgsimageoperation.h:66
QgsImageOperation::gaussianBlur
static QImage * gaussianBlur(QImage &image, int radius, QgsFeedback *feedback=nullptr)
Performs a gaussian blur on an image.
Definition: qgsimageoperation.cpp:651
QgsImageOperation::flipImage
static void flipImage(QImage &image, FlipType type)
Flips an image horizontally or vertically.
Definition: qgsimageoperation.cpp:843
qgscolorramp.h
qgsDoubleNear
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition: qgis.h:2265
QgsFeedback
Base class for feedback objects to be used for cancellation of something running in a worker thread.
Definition: qgsfeedback.h:44
QgsImageOperation::adjustHueSaturation
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.
Definition: qgsimageoperation.cpp:280
QgsImageOperation::DistanceTransformProperties::shadeExterior
bool shadeExterior
Set to true to perform the distance transform on transparent pixels in the source image,...
Definition: qgsimageoperation.h:130
BLOCK_THREADS
#define BLOCK_THREADS
Definition: qgsimageoperation.cpp:29
INF
#define INF
Definition: qgsimageoperation.cpp:31
QgsImageOperation::DistanceTransformProperties::spread
double spread
Maximum distance (in pixels) for the distance transform shading to spread.
Definition: qgsimageoperation.h:142
QgsImageOperation::DistanceTransformProperties
Struct for storing properties of a distance transform operation.
Definition: qgsimageoperation.h:122
QgsImageOperation::multiplyOpacity
static void multiplyOpacity(QImage &image, double factor, QgsFeedback *feedback=nullptr)
Multiplies opacity of image pixel values by a factor.
Definition: qgsimageoperation.cpp:335
QgsImageOperation::FlipHorizontal
@ FlipHorizontal
Flip the image horizontally.
Definition: qgsimageoperation.h:68
QgsImageOperation::nonTransparentImageRect
static QRect nonTransparentImageRect(const QImage &image, QSize minSize=QSize(), bool center=false)
Calculates the non-transparent region of an image.
Definition: qgsimageoperation.cpp:850
QgsImageOperation::stackBlur
static void stackBlur(QImage &image, int radius, bool alphaOnly=false, QgsFeedback *feedback=nullptr)
Performs a stack blur on an image.
Definition: qgsimageoperation.cpp:585
qgslogger.h
QgsImageOperation::overlayColor
static void overlayColor(QImage &image, const QColor &color)
Overlays a color onto an image.
Definition: qgsimageoperation.cpp:371
qgsfeedback.h
QgsImageOperation::GrayscaleMode
GrayscaleMode
Modes for converting a QImage to grayscale.
Definition: qgsimageoperation.h:55
QgsImageOperation::convertToGrayscale
static void convertToGrayscale(QImage &image, GrayscaleMode mode=GrayscaleLuminosity, QgsFeedback *feedback=nullptr)
Convert a QImage to a grayscale image.
Definition: qgsimageoperation.cpp:195
QgsImageOperation::adjustBrightnessContrast
static void adjustBrightnessContrast(QImage &image, int brightness, double contrast, QgsFeedback *feedback=nullptr)
Alter the brightness or contrast of a QImage.
Definition: qgsimageoperation.cpp:256
QgsImageOperation::distanceTransform
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.
Definition: qgsimageoperation.cpp:387
qgssize
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:2791