New QgsImageOperation class for operations that modify QImages

Contains framework for multithreaded operations on QImages, and
numerous operations such as grayscale, hue/saturation, brightness/
contrast modification, flip, blur, distance transform, alpha
modification and color overlays.
This commit is contained in:
Nyall Dawson 2015-01-16 21:21:14 +11:00
parent 3f88ee5213
commit be2d6d1a70
34 changed files with 1497 additions and 0 deletions

View File

@ -81,6 +81,7 @@ INCLUDE_DIRECTORIES(
../src/core/pal
../src/core/composer
../src/core/diagram
../src/core/effects
../src/core/dxf
../src/core/gps
../src/core/layertree

View File

@ -170,6 +170,8 @@
%Include diagram/qgspiediagram.sip
%Include diagram/qgstextdiagram.sip
%Include effects/qgsimageoperation.sip
%Include gps/qgsgpsconnection.sip
%Include gps/qgsgpsconnectionregistry.sip
%Include gps/qgsgpsdconnection.sip

View File

@ -0,0 +1,117 @@
/** \ingroup core
* \class QgsImageOperation
* \brief Contains operations and filters which apply to QImages
*
* A set of optimised pixel manipulation operations and filters which can be applied
* to QImages.
* \note Added in version 2.7
*/
class QgsImageOperation
{
%TypeHeaderCode
#include <qgsimageoperation.h>
%End
public:
/** Modes for converting a QImage to grayscale
*/
enum GrayscaleMode
{
GrayscaleLightness, /*< keep the lightness of the color, drops the saturation */
GrayscaleLuminosity, /*< grayscale by perceptual luminosity (weighted sum of color RGB components) */
GrayscaleAverage /*< grayscale by taking average of color RGB components */
};
/** Flip operation types
*/
enum FlipType
{
FlipHorizontal, /*< flip the image horizontally */
FlipVertical /*< flip the image vertically */
};
/**Convert a QImage to a grayscale image. Alpha channel is preserved.
* @param image QImage to convert
* @param mode mode to use during grayscale conversion
*/
static void convertToGrayscale( QImage &image, const GrayscaleMode mode = GrayscaleLuminosity );
/**Alter the brightness or contrast of a QImage.
* @param image QImage to alter
* @param brightness brightness value, in the range -255 to 255. A brightness value of 0 indicates
* no change to brightness, a negative value will darken the image, and a positive value will brighten
* the image.
* @param contrast contrast value. Must be a positive or zero value. A value of 1.0 indicates no change
* to the contrast, a value of 0 represents an image with 0 contrast, and a value > 1.0 will increase the
* contrast of the image.
*/
static void adjustBrightnessContrast( QImage &image, const int brightness, const double contrast );
/**Alter the hue or saturation of a QImage.
* @param image QImage to alter
* @param saturation double between 0 and 2 inclusive, where 0 = desaturate and 1.0 = no change
* @param colorizeColor color to use for colorizing image. Set to an invalid QColor to disable
* colorization.
* @param colorizeStrength double between 0 and 1, where 0 = no colorization and 1.0 = full colorization
*/
static void adjustHueSaturation( QImage &image, const double saturation, const QColor& colorizeColor = QColor(),
const double colorizeStrength = 1.0 );
/**Multiplies opacity of image pixel values by a factor.
* @param image QImage to alter
* @param factor factor to multiple pixel's opacity by
*/
static void multiplyOpacity( QImage &image, const double factor );
/**Overlays a color onto an image. This operation retains the alpha channel of the
* original image, but replaces all image pixel colors with the specified color.
* @param image QImage to alter
* @param color color to overlay (any alpha component of the color is ignored)
*/
static void overlayColor( QImage &image, const QColor& color );
/**Struct for storing properties of a distance transform operation*/
struct DistanceTransformProperties
{
/**Set to true to perform the distance transform on transparent pixels
* in the source image, set to false to perform the distance transform
* on opaque pixels
*/
bool shadeExterior;
/**Set to true to automatically calculate the maximum distance in the
* transform to use as the spread value
*/
bool useMaxDistance;
/**Maximum distance (in pixels) for the distance transform shading to
* spread
*/
double spread;
/**Color ramp to use for shading the distance transform
*/
QgsVectorColorRampV2* ramp;
};
/**Performs a distance transform on the source image and shades the result
* using a color ramp.
* @param image QImage to alter
* @param properties DistanceTransformProperties object with parameters
* for the distance transform operation
*/
static void distanceTransform( QImage &image, const QgsImageOperation::DistanceTransformProperties& properties );
/**Performs a stack blur on an image. Stack blur represents a good balance between
* speed and blur quality.
* @param image QImage to blur
* @param radius blur radius in pixels, maximum value of 16
* @param alphaOnly set to true to blur only the alpha component of the image
*/
static void stackBlur( QImage &image, const int radius, const bool alphaOnly = false );
/**Flips an image horizontally or vertically
* @param image QImage to flip
* @param type type of flip to perform (horizontal or vertical)
*/
static void flipImage( QImage &image, FlipType type );
};

View File

@ -46,6 +46,8 @@ SET(QGIS_CORE_SRCS
diagram/qgspiediagram.cpp
diagram/qgstextdiagram.cpp
diagram/qgshistogramdiagram.cpp
effects/qgsimageoperation.cpp
layertree/qgslayertreegroup.cpp
layertree/qgslayertreelayer.cpp
@ -554,6 +556,8 @@ SET(QGIS_CORE_HDRS
diagram/qgspiediagram.h
diagram/qgstextdiagram.h
diagram/qgshistogramdiagram.h
effects/qgsimageoperation.h
composer/qgsaddremovemultiframecommand.h
composer/qgscomposerarrow.h
@ -647,6 +651,7 @@ INCLUDE_DIRECTORIES(
${CMAKE_CURRENT_SOURCE_DIR}
composer
dxf
effects
layertree
pal
raster

View File

@ -0,0 +1,585 @@
/***************************************************************************
qgsimageoperation.cpp
----------------------
begin : January 2015
copyright : (C) 2015 by Nyall Dawson
email : nyall.dawson@gmail.com
***************************************************************************/
/***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#include "qgsimageoperation.h"
#include "qgis.h"
#include "qgsvectorcolorrampv2.h"
#include "qgslogger.h"
#include <QtConcurrentMap>
#include <QColor>
#include <QPainter>
#include <qmath.h>
//determined via trial-and-error. Could possibly be optimised, or varied
//depending on the image size.
#define BLOCK_THREADS 16
#define INF 1E20
template <typename PixelOperation>
void QgsImageOperation::runPixelOperation( QImage &image, PixelOperation& operation )
{
if ( image.height() * image.width() < 100000 )
{
//small image, don't multithread
//this threshold was determined via testing various images
runPixelOperationOnWholeImage( image, operation );
}
else
{
//large image, multithread operation
QgsImageOperation::ProcessBlockUsingPixelOperation<PixelOperation> blockOp( operation ) ;
runBlockOperationInThreads( image, blockOp, QgsImageOperation::ByRow );
}
}
template <typename PixelOperation>
void QgsImageOperation::runPixelOperationOnWholeImage( QImage &image, PixelOperation& operation )
{
int height = image.height();
int width = image.width();
for ( int y = 0; y < height; ++y )
{
QRgb* ref = ( QRgb* )image.scanLine( y );
for ( int x = 0; x < width; ++x )
{
operation( ref[x], x, y );
}
}
}
//linear operations
template <typename LineOperation>
void QgsImageOperation::runLineOperation( QImage &image, LineOperation& operation )
{
//possibly could be tweaked for rect operations
if ( image.height() * image.width() < 100000 )
{
//small image, don't multithread
//this threshold was determined via testing various images
runLineOperationOnWholeImage( image, operation );
}
else
{
//large image, multithread operation
QgsImageOperation::ProcessBlockUsingLineOperation<LineOperation> blockOp( operation ) ;
runBlockOperationInThreads( image, blockOp, operation.direction() );
}
}
template <class LineOperation>
void QgsImageOperation::runLineOperationOnWholeImage( QImage &image, LineOperation& operation )
{
int height = image.height();
int width = image.width();
//do something with whole lines
int bpl = image.bytesPerLine();
if ( operation.direction() == ByRow )
{
for ( int y = 0; y < height; ++y )
{
QRgb* ref = ( QRgb* )image.scanLine( y );
operation( ref, width, bpl );
}
}
else
{
//by column
unsigned char* ref = image.scanLine( 0 );
for ( int x = 0; x < width; ++x, ref += 4 )
{
operation(( QRgb* )ref, height, bpl );
}
}
}
//multithreaded block processing
template <typename BlockOperation>
void QgsImageOperation::runBlockOperationInThreads( QImage &image, BlockOperation &operation, LineOperationDirection direction )
{
QList< ImageBlock > blocks;
unsigned int height = image.height();
unsigned int width = image.width();
unsigned int blockDimension1 = ( direction == QgsImageOperation::ByRow ) ? height : width;
unsigned int blockDimension2 = ( direction == QgsImageOperation::ByRow ) ? width : height;
//chunk image up into vertical blocks
blocks.reserve( BLOCK_THREADS );
unsigned int begin = 0;
unsigned int blockLen = blockDimension1 / BLOCK_THREADS;
for ( unsigned int block = 0; block < BLOCK_THREADS; ++block, begin += blockLen )
{
ImageBlock newBlock;
newBlock.beginLine = begin;
//make sure last block goes to end of image
newBlock.endLine = block < ( BLOCK_THREADS - 1 ) ? begin + blockLen : blockDimension1;
newBlock.lineLength = blockDimension2;
newBlock.image = &image;
blocks << newBlock;
}
//process blocks
QtConcurrent::blockingMap( blocks, operation );
}
//
//operation specific code
//
//grayscale
void QgsImageOperation::convertToGrayscale( QImage &image, const GrayscaleMode mode )
{
GrayscalePixelOperation operation( mode );
runPixelOperation( image, operation );
}
void QgsImageOperation::GrayscalePixelOperation::operator()( QRgb &rgb, const int x, const int y )
{
Q_UNUSED( x );
Q_UNUSED( y );
switch ( mMode )
{
case GrayscaleLuminosity:
grayscaleLuminosityOp( rgb );
return;
case GrayscaleAverage:
grayscaleAverageOp( rgb );
return;
case GrayscaleLightness:
default:
grayscaleLightnessOp( rgb );
return;
}
}
void QgsImageOperation::grayscaleLightnessOp( QRgb &rgb )
{
int red = qRed( rgb );
int green = qGreen( rgb );
int blue = qBlue( rgb );
int min = qMin( qMin( red, green ), blue );
int max = qMax( qMax( red, green ), blue );
int lightness = qMin(( min + max ) / 2, 255 );
rgb = qRgba( lightness, lightness, lightness, qAlpha( rgb ) );
}
void QgsImageOperation::grayscaleLuminosityOp( QRgb &rgb )
{
int luminosity = 0.21 * qRed( rgb ) + 0.72 * qGreen( rgb ) + 0.07 * qBlue( rgb );
rgb = qRgba( luminosity, luminosity, luminosity, qAlpha( rgb ) );
}
void QgsImageOperation::grayscaleAverageOp( QRgb &rgb )
{
int average = ( qRed( rgb ) + qGreen( rgb ) + qBlue( rgb ) ) / 3;
rgb = qRgba( average, average, average, qAlpha( rgb ) );
}
//brightness/contrast
void QgsImageOperation::adjustBrightnessContrast( QImage &image, const int brightness, const double contrast )
{
BrightnessContrastPixelOperation operation( brightness, contrast );
runPixelOperation( image, operation );
}
void QgsImageOperation::BrightnessContrastPixelOperation::operator()( QRgb &rgb, const int x, const int y )
{
Q_UNUSED( x );
Q_UNUSED( y );
int red = adjustColorComponent( qRed( rgb ), mBrightness, mContrast );
int blue = adjustColorComponent( qBlue( rgb ), mBrightness, mContrast );
int green = adjustColorComponent( qGreen( rgb ), mBrightness, mContrast );
rgb = qRgba( red, green, blue, qAlpha( rgb ) );
}
int QgsImageOperation::adjustColorComponent( int colorComponent, int brightness, double contrastFactor )
{
return qBound( 0, ( int )(((((( colorComponent / 255.0 ) - 0.5 ) * contrastFactor ) + 0.5 ) * 255 ) + brightness ), 255 );
}
//hue/saturation
void QgsImageOperation::adjustHueSaturation( QImage &image, const double saturation, const QColor &colorizeColor, const double colorizeStrength )
{
HueSaturationPixelOperation operation( saturation, colorizeColor.isValid() && colorizeStrength > 0.0,
colorizeColor.hue(), colorizeColor.saturation(), colorizeStrength );
runPixelOperation( image, operation );
}
void QgsImageOperation::HueSaturationPixelOperation::operator()( QRgb &rgb, const int x, const int y )
{
Q_UNUSED( x );
Q_UNUSED( y );
QColor tmpColor( rgb );
int h, s, l;
tmpColor.getHsl( &h, &s, &l );
if ( mSaturation < 1.0 )
{
// Lowering the saturation. Use a simple linear relationship
s = qMin(( int )( s * mSaturation ), 255 );
}
else if ( mSaturation > 1.0 )
{
// Raising the saturation. Use a saturation curve to prevent
// clipping at maximum saturation with ugly results.
s = qMin(( int )( 255. * ( 1 - qPow( 1 - ( s / 255. ), qPow( mSaturation, 2 ) ) ) ), 255 );
}
if ( mColorize )
{
h = mColorizeHue;
s = mColorizeSaturation;
if ( mColorizeStrength < 1.0 )
{
//get rgb for colorized color
QColor colorizedColor = QColor::fromHsl( h, s, l );
int colorizedR, colorizedG, colorizedB;
colorizedColor.getRgb( &colorizedR, &colorizedG, &colorizedB );
// Now, linearly scale by colorize strength
int r = mColorizeStrength * colorizedR + ( 1 - mColorizeStrength ) * tmpColor.red();
int g = mColorizeStrength * colorizedG + ( 1 - mColorizeStrength ) * tmpColor.green();
int b = mColorizeStrength * colorizedB + ( 1 - mColorizeStrength ) * tmpColor.blue();
rgb = qRgba( r, g, b, qAlpha( rgb ) );
return;
}
}
tmpColor.setHsl( h, s, l, qAlpha( rgb ) );
rgb = tmpColor.rgba();
}
//multiply opacity
void QgsImageOperation::multiplyOpacity( QImage &image, const double factor )
{
if ( qgsDoubleNear( factor, 1.0 ) )
{
//no change
return;
}
else if ( factor < 1.0 )
{
//decreasing opacity - we can use the faster DestinationIn composition mode
//to reduce the alpha channel
QColor transparentFillColor = QColor( 0, 0, 0, 255 * factor );
QPainter painter( &image );
painter.setCompositionMode( QPainter::CompositionMode_DestinationIn );
painter.fillRect( 0, 0, image.width(), image.height(), transparentFillColor );
painter.end();
}
else
{
//increasing opacity - run this as a pixel operation for multithreading
MultiplyOpacityPixelOperation operation( factor );
runPixelOperation( image, operation );
}
}
void QgsImageOperation::MultiplyOpacityPixelOperation::operator()( QRgb &rgb, const int x, const int y )
{
Q_UNUSED( x );
Q_UNUSED( y );
rgb = qRgba( qRed( rgb ), qGreen( rgb ), qBlue( rgb ), qBound( 0, qRound( mFactor * qAlpha( rgb ) ), 255 ) );
}
// overlay color
void QgsImageOperation::overlayColor( QImage &image, const QColor &color )
{
QColor opaqueColor = color;
opaqueColor.setAlpha( 255 );
//use QPainter SourceIn composition mode to overlay color (fast)
//this retains image's alpha channel but replaces color
QPainter painter( &image );
painter.setCompositionMode( QPainter::CompositionMode_SourceIn );
painter.fillRect( 0, 0, image.width(), image.height(), opaqueColor );
painter.end();
}
// distance transform
void QgsImageOperation::distanceTransform( QImage &image, const DistanceTransformProperties& properties )
{
if ( ! properties.ramp )
{
QgsDebugMsg( QString( "no color ramp specified for distance transform" ) );
return;
}
//first convert to 1 bit alpha mask array
double * array = new double[ image.width() * image.height()];
ConvertToArrayPixelOperation convertToArray( image.width(), array, properties.shadeExterior );
runPixelOperation( image, convertToArray );
//calculate distance transform (single threaded only)
distanceTransform2d( array, image.width(), image.height() );
double spread;
if ( properties.useMaxDistance )
{
spread = sqrt( maxValueInDistanceTransformArray( array, image.width() * image.height() ) );
}
else
{
spread = properties.spread;
}
//shade distance transform
ShadeFromArrayOperation shadeFromArray( image.width(), array, spread, properties );
runPixelOperation( image, shadeFromArray );
delete [] array;
}
void QgsImageOperation::ConvertToArrayPixelOperation::operator()( QRgb &rgb, const int x, const int y )
{
// TODO - try initial distance = 1-alphaF, INF for alphaF = 0 only
int idx = y * mWidth + x;
if (( mExterior && qAlpha( rgb ) >= mAlphaThreshold ) || ( !mExterior && qAlpha( rgb ) < mAlphaThreshold ) )
{
//opaque pixel, so zero distance
mArray[ idx ] = 0;
}
else
{
//transparent pixel, so initially set distance as infinite
mArray[ idx ] = INF;
}
}
//fast distance transform code, adapted from http://cs.brown.edu/~pff/dt/
/* distance transform of a 1d function using squared distance */
void QgsImageOperation::distanceTransform1d( double *f, int n, int *v, double *z, double *d )
{
int k = 0;
v[0] = 0;
z[0] = -INF;
z[1] = + INF;
for ( int q = 1; q <= n - 1; q++ )
{
double s = (( f[q] + q * q ) - ( f[v[k]] + ( v[k] * v[k] ) ) ) / ( 2 * q - 2 * v[k] );
while ( s <= z[k] )
{
k--;
s = (( f[q] + q * q ) - ( f[v[k]] + ( v[k] * v[k] ) ) ) / ( 2 * q - 2 * v[k] );
}
k++;
v[k] = q;
z[k] = s;
z[k+1] = + INF;
}
k = 0;
for ( int q = 0; q <= n - 1; q++ )
{
while ( z[k+1] < q )
k++;
d[q] = ( q - v[k] ) * ( q - v[k] ) + f[v[k]];
}
}
double QgsImageOperation::maxValueInDistanceTransformArray( const double *array, const unsigned int size )
{
double dtMaxValue = array[0];
for ( unsigned int i = 1; i < size; ++i )
{
if ( array[i] > dtMaxValue )
{
dtMaxValue = array[i];
}
}
return dtMaxValue;
}
/* distance transform of 2d function using squared distance */
void QgsImageOperation::distanceTransform2d( double * im, int width, int height )
{
int maxDimension = qMax( width, height );
double *f = new double[ maxDimension ];
int *v = new int[ maxDimension ];
double *z = new double[ maxDimension + 1 ];
double *d = new double[ maxDimension ];
// transform along columns
for ( int x = 0; x < width; x++ )
{
for ( int y = 0; y < height; y++ )
{
f[y] = im[ x + y * width ];
}
distanceTransform1d( f, height, v, z, d );
for ( int y = 0; y < height; y++ )
{
im[ x + y * width ] = d[y];
}
}
// transform along rows
for ( int y = 0; y < height; y++ )
{
for ( int x = 0; x < width; x++ )
{
f[x] = im[ x + y*width ];
}
distanceTransform1d( f, width, v, z, d );
for ( int x = 0; x < width; x++ )
{
im[ x + y*width ] = d[x];
}
}
delete [] d;
delete [] f;
delete [] v;
delete [] z;
}
void QgsImageOperation::ShadeFromArrayOperation::operator()( QRgb &rgb, const int x, const int y )
{
if ( ! mProperties.ramp )
return;
if ( mSpread == 0 )
{
rgb = mProperties.ramp->color( 1.0 ).rgba();
return;
}
int idx = y * mWidth + x;
//values are distance squared
double squaredVal = mArray[ idx ];
if ( squaredVal > mSpreadSquared )
{
rgb = Qt::transparent;
return;
}
double val = squaredVal > 0 ? qMin(( sqrt( squaredVal ) / mSpread ), 1.0 ) : 0;
rgb = mProperties.ramp->color( val ).rgba();
}
//stack blur
void QgsImageOperation::stackBlur( QImage &image, const int radius, const bool alphaOnly )
{
// culled from Qt's qpixmapfilter.cpp, see: http://www.qtcentre.org/archive/index.php/t-26534.html
int tab[] = { 14, 10, 8, 6, 5, 5, 4, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2 };
int alpha = ( radius < 1 ) ? 16 : ( radius > 17 ) ? 1 : tab[radius-1];
int i1 = 0;
int i2 = 3;
if ( alphaOnly ) // this seems to only work right for a black color
i1 = i2 = ( QSysInfo::ByteOrder == QSysInfo::BigEndian ? 0 : 3 );
StackBlurLineOperation topToBottomBlur( alpha, QgsImageOperation::ByColumn, true, i1, i2 );
runLineOperation( image, topToBottomBlur );
StackBlurLineOperation leftToRightBlur( alpha, QgsImageOperation::ByRow, true, i1, i2 );
runLineOperation( image, leftToRightBlur );
StackBlurLineOperation bottomToTopBlur( alpha, QgsImageOperation::ByColumn, false, i1, i2 );
runLineOperation( image, bottomToTopBlur );
StackBlurLineOperation rightToLeftBlur( alpha, QgsImageOperation::ByRow, false, i1, i2 );
runLineOperation( image, rightToLeftBlur );
}
void QgsImageOperation::StackBlurLineOperation::operator()( QRgb* startRef, const int lineLength, const int bytesPerLine )
{
unsigned char* p = ( unsigned char* )startRef;
int rgba[4];
int increment = ( mDirection == QgsImageOperation::ByRow ) ? 4 : bytesPerLine;
if ( !mForwardDirection )
{
p += ( lineLength - 1 ) * increment;
increment = -increment;
}
for ( int i = mi1; i <= mi2; ++i )
{
rgba[i] = p[i] << 4;
}
p += increment;
for ( int j = 1; j < lineLength; ++j, p += increment )
{
for ( int i = mi1; i <= mi2; ++i )
{
p[i] = ( rgba[i] += (( p[i] << 4 ) - rgba[i] ) * mAlpha / 16 ) >> 4;
}
}
}
// flip
void QgsImageOperation::flipImage( QImage &image, QgsImageOperation::FlipType type )
{
FlipLineOperation flipOperation( type == QgsImageOperation::FlipHorizontal ? QgsImageOperation::ByRow : QgsImageOperation::ByColumn );
runLineOperation( image, flipOperation );
}
void QgsImageOperation::FlipLineOperation::operator()( QRgb *startRef, const int lineLength, const int bytesPerLine )
{
int increment = ( mDirection == QgsImageOperation::ByRow ) ? 4 : bytesPerLine;
//store temporary line
unsigned char* p = ( unsigned char* )startRef;
unsigned char* tempLine = new unsigned char[ lineLength * 4 ];
for ( int i = 0; i < lineLength * 4; ++i, p += increment )
{
tempLine[i++] = *( p++ );
tempLine[i++] = *( p++ );
tempLine[i++] = *( p++ );
tempLine[i] = *( p );
p -= 3;
}
//write values back in reverse order
p = ( unsigned char* )startRef;
for ( int i = ( lineLength - 1 ) * 4; i >= 0; i -= 7, p += increment )
{
*( p++ ) = tempLine[i++];
*( p++ ) = tempLine[i++];
*( p++ ) = tempLine[i++];
*( p ) = tempLine[i];
p -= 3;
}
delete[] tempLine;
}

View File

@ -0,0 +1,402 @@
/***************************************************************************
qgsimageoperation.h
--------------------
begin : January 2015
copyright : (C) 2015 by Nyall Dawson
email : nyall.dawson@gmail.com
***************************************************************************/
/***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#ifndef QGSIMAGEOPERATION_H
#define QGSIMAGEOPERATION_H
#include <QImage>
#include <QColor>
#include <QtCore/qmath.h>
class QgsVectorColorRampV2;
/** \ingroup core
* \class QgsImageOperation
* \brief Contains operations and filters which apply to QImages
*
* A set of optimised pixel manipulation operations and filters which can be applied
* to QImages. All operations only apply to ARGB32 format images, and it is left up
* to the calling procedure to ensure that any passed images are of the correct
* format.
*
* \note These operations do not work using premultiplied ARGB32_Premultiplied images
* - please make sure the images are converted to standard ARGB32 images prior to calling
* these operations.
*
* \note Added in version 2.7
*/
class CORE_EXPORT QgsImageOperation
{
public:
/** Modes for converting a QImage to grayscale
*/
enum GrayscaleMode
{
GrayscaleLightness, /*< keep the lightness of the color, drops the saturation */
GrayscaleLuminosity, /*< grayscale by perceptual luminosity (weighted sum of color RGB components) */
GrayscaleAverage /*< grayscale by taking average of color RGB components */
};
/** Flip operation types
*/
enum FlipType
{
FlipHorizontal, /*< flip the image horizontally */
FlipVertical /*< flip the image vertically */
};
/**Convert a QImage to a grayscale image. Alpha channel is preserved.
* @param image QImage to convert
* @param mode mode to use during grayscale conversion
*/
static void convertToGrayscale( QImage &image, const GrayscaleMode mode = GrayscaleLuminosity );
/**Alter the brightness or contrast of a QImage.
* @param image QImage to alter
* @param brightness brightness value, in the range -255 to 255. A brightness value of 0 indicates
* no change to brightness, a negative value will darken the image, and a positive value will brighten
* the image.
* @param contrast contrast value. Must be a positive or zero value. A value of 1.0 indicates no change
* to the contrast, a value of 0 represents an image with 0 contrast, and a value > 1.0 will increase the
* contrast of the image.
*/
static void adjustBrightnessContrast( QImage &image, const int brightness, const double contrast );
/**Alter the hue or saturation of a QImage.
* @param image QImage to alter
* @param saturation double between 0 and 2 inclusive, where 0 = desaturate and 1.0 = no change
* @param colorizeColor color to use for colorizing image. Set to an invalid QColor to disable
* colorization.
* @param colorizeStrength double between 0 and 1, where 0 = no colorization and 1.0 = full colorization
*/
static void adjustHueSaturation( QImage &image, const double saturation, const QColor& colorizeColor = QColor(),
const double colorizeStrength = 1.0 );
/**Multiplies opacity of image pixel values by a factor.
* @param image QImage to alter
* @param factor factor to multiple pixel's opacity by
*/
static void multiplyOpacity( QImage &image, const double factor );
/**Overlays a color onto an image. This operation retains the alpha channel of the
* original image, but replaces all image pixel colors with the specified color.
* @param image QImage to alter
* @param color color to overlay (any alpha component of the color is ignored)
*/
static void overlayColor( QImage &image, const QColor& color );
/**Struct for storing properties of a distance transform operation*/
struct DistanceTransformProperties
{
DistanceTransformProperties()
: shadeExterior( true )
, useMaxDistance( true )
, spread( 10.0 )
, ramp( NULL )
{ }
/**Set to true to perform the distance transform on transparent pixels
* in the source image, set to false to perform the distance transform
* on opaque pixels
*/
bool shadeExterior;
/**Set to true to automatically calculate the maximum distance in the
* transform to use as the spread value
*/
bool useMaxDistance;
/**Maximum distance (in pixels) for the distance transform shading to
* spread
*/
double spread;
/**Color ramp to use for shading the distance transform
*/
QgsVectorColorRampV2* ramp;
};
/**Performs a distance transform on the source image and shades the result
* using a color ramp.
* @param image QImage to alter
* @param properties DistanceTransformProperties object with parameters
* for the distance transform operation
*/
static void distanceTransform( QImage &image, const DistanceTransformProperties& properties );
/**Performs a stack blur on an image. Stack blur represents a good balance between
* speed and blur quality.
* @param image QImage to blur
* @param radius blur radius in pixels, maximum value of 16
* @param alphaOnly set to true to blur only the alpha component of the image
*/
static void stackBlur( QImage &image, const int radius, const bool alphaOnly = false );
/**Flips an image horizontally or vertically
* @param image QImage to flip
* @param type type of flip to perform (horizontal or vertical)
*/
static void flipImage( QImage &image, FlipType type );
private:
//for blocked operations
enum LineOperationDirection
{
ByRow,
ByColumn
};
template <class BlockOperation> static void runBlockOperationInThreads( QImage &image, BlockOperation& operation, LineOperationDirection direction );
struct ImageBlock
{
unsigned int beginLine;
unsigned int endLine;
unsigned int lineLength;
QImage* image;
};
//for per pixel operations
template <class PixelOperation> static void runPixelOperation( QImage &image, PixelOperation& operation );
template <class PixelOperation> static void runPixelOperationOnWholeImage( QImage &image, PixelOperation& operation );
template <class PixelOperation>
struct ProcessBlockUsingPixelOperation
{
ProcessBlockUsingPixelOperation( PixelOperation& operation )
: mOperation( operation ) { }
typedef void result_type;
void operator()( ImageBlock& block )
{
for ( unsigned int y = block.beginLine; y < block.endLine; ++y )
{
QRgb* ref = ( QRgb* )block.image->scanLine( y );
for ( unsigned int x = 0; x < block.lineLength; ++x )
{
mOperation( ref[x], x, y );
}
}
}
PixelOperation& mOperation;
};
//for linear operations
template <typename LineOperation> static void runLineOperation( QImage &image, LineOperation& operation );
template <class LineOperation> static void runLineOperationOnWholeImage( QImage &image, LineOperation& operation );
template <class LineOperation>
struct ProcessBlockUsingLineOperation
{
ProcessBlockUsingLineOperation( LineOperation& operation )
: mOperation( operation ) { }
typedef void result_type;
void operator()( ImageBlock& block )
{
//do something with whole lines
int bpl = block.image->bytesPerLine();
if ( mOperation.direction() == ByRow )
{
for ( unsigned int y = block.beginLine; y < block.endLine; ++y )
{
QRgb* ref = ( QRgb* )block.image->scanLine( y );
mOperation( ref, block.lineLength, bpl );
}
}
else
{
//by column
unsigned char* ref = block.image->scanLine( 0 ) + 4 * block.beginLine;
for ( unsigned int x = block.beginLine; x < block.endLine; ++x, ref += 4 )
{
mOperation(( QRgb* )ref, block.lineLength, bpl );
}
}
}
LineOperation& mOperation;
};
//individual operation implementations
class GrayscalePixelOperation
{
public:
GrayscalePixelOperation( const GrayscaleMode mode )
: mMode( mode )
{ }
void operator()( QRgb& rgb, const int x, const int y );
private:
GrayscaleMode mMode;
};
static void grayscaleLightnessOp( QRgb& rgb );
static void grayscaleLuminosityOp( QRgb& rgb );
static void grayscaleAverageOp( QRgb& rgb );
class BrightnessContrastPixelOperation
{
public:
BrightnessContrastPixelOperation( const int brightness, const double contrast )
: mBrightness( brightness )
, mContrast( contrast )
{ }
void operator()( QRgb& rgb, const int x, const int y );
private:
int mBrightness;
double mContrast;
};
class HueSaturationPixelOperation
{
public:
HueSaturationPixelOperation( const double saturation, const bool colorize,
const int colorizeHue, const int colorizeSaturation,
const double colorizeStrength )
: mSaturation( saturation )
, mColorize( colorize )
, mColorizeHue( colorizeHue )
, mColorizeSaturation( colorizeSaturation )
, mColorizeStrength( colorizeStrength )
{ }
void operator()( QRgb& rgb, const int x, const int y );
private:
double mSaturation; // [0, 2], 1 = no change
bool mColorize;
int mColorizeHue;
int mColorizeSaturation;
double mColorizeStrength; // [0,1]
};
static int adjustColorComponent( int colorComponent, int brightness, double contrastFactor );
class MultiplyOpacityPixelOperation
{
public:
MultiplyOpacityPixelOperation( const double factor )
: mFactor( factor )
{ }
void operator()( QRgb& rgb, const int x, const int y );
private:
double mFactor;
};
class ConvertToArrayPixelOperation
{
public:
ConvertToArrayPixelOperation( const int width, double * array, const bool exterior = true, const int alphaThreshold = 255 )
: mWidth( width )
, mArray( array )
, mExterior( exterior )
, mAlphaThreshold( alphaThreshold )
{
}
void operator()( QRgb& rgb, const int x, const int y );
private:
int mWidth;
double * mArray;
bool mExterior;
int mAlphaThreshold;
};
class ShadeFromArrayOperation
{
public:
ShadeFromArrayOperation( const int width, double* array, const double spread,
const DistanceTransformProperties& properties )
: mWidth( width )
, mArray( array )
, mSpread( spread )
, mProperties( properties )
{
mSpreadSquared = qPow( mSpread, 2.0 );
}
void operator()( QRgb& rgb, const int x, const int y );
private:
int mWidth;
double * mArray;
double mSpread;
double mSpreadSquared;
const DistanceTransformProperties& mProperties;
};
static void distanceTransform2d( double *im, int width, int height );
static void distanceTransform1d( double *f, int n, int *v, double *z, double *d );
static double maxValueInDistanceTransformArray( const double *array, const unsigned int size );
class StackBlurLineOperation
{
public:
StackBlurLineOperation( int alpha, LineOperationDirection direction, bool forwardDirection, int i1, int i2 )
: mAlpha( alpha )
, mDirection( direction )
, mForwardDirection( forwardDirection )
, mi1( i1 )
, mi2( i2 )
{ }
typedef void result_type;
LineOperationDirection direction() { return mDirection; }
void operator()( QRgb* startRef, const int lineLength, const int bytesPerLine );
private:
int mAlpha;
LineOperationDirection mDirection;
bool mForwardDirection;
int mi1;
int mi2;
};
class FlipLineOperation
{
public:
FlipLineOperation( LineOperationDirection direction )
: mDirection( direction )
{ }
typedef void result_type;
LineOperationDirection direction() { return mDirection; }
void operator()( QRgb* startRef, const int lineLength, const int bytesPerLine );
private:
LineOperationDirection mDirection;
};
};
#endif // QGSIMAGEOPERATION_H

View File

@ -9,6 +9,7 @@ INCLUDE_DIRECTORIES(${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_BINARY_DIR}
${CMAKE_SOURCE_DIR}/src/core
${CMAKE_SOURCE_DIR}/src/core/composer
${CMAKE_SOURCE_DIR}/src/core/effects
${CMAKE_SOURCE_DIR}/src/core/layertree
${CMAKE_SOURCE_DIR}/src/core/raster
${CMAKE_SOURCE_DIR}/src/core/symbology-ng
@ -145,3 +146,4 @@ ADD_QGIS_TEST(vectorlayerjoinbuffer testqgsvectorlayerjoinbuffer.cpp )
ADD_QGIS_TEST(maplayerstylemanager testqgsmaplayerstylemanager.cpp )
ADD_QGIS_TEST(pointlocatortest testqgspointlocator.cpp )
ADD_QGIS_TEST(snappingutilstest testqgssnappingutils.cpp )
ADD_QGIS_TEST(imageoperationtest testqgsimageoperation.cpp)

View File

@ -0,0 +1,383 @@
/***************************************************************************
testqgsimageoperations.cpp
--------------------------
begin : January 2015
copyright : (C) 2015 by Nyall Dawson
email : nyall dot dawson at gmail dot com
***************************************************************************/
/***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#include "qgsimageoperation.h"
#include "qgsvectorcolorrampv2.h"
#include <QObject>
#include <QtTest/QtTest>
#include "qgsrenderchecker.h"
class TestQgsImageOperation : public QObject
{
Q_OBJECT
private slots:
void initTestCase();// will be called before the first testfunction is executed.
void cleanupTestCase();// will be called after the last testfunction was executed.
void init();// will be called before each testfunction is executed.
void cleanup();// will be called after every testfunction.
void smallImageOp(); //test operation on small image (single threaded op)
//grayscale
void grayscaleLightness();
void grayscaleLuminosity();
void grayscaleAverage();
//brightness/contrast
void brightnessContrastNoChange();
void increaseBrightness();
void decreaseBrightness();
void increaseContrast();
void decreaseContrast();
//hue/saturation
void hueSaturationNoChange();
void increaseSaturation();
void decreaseSaturation();
void colorizeFull();
void colorizePartial();
//multiply opacity
void opacityNoChange();
void opacityIncrease();
void opacityDecrease();
//overlay color
void overlayColor();
//distance transform
void distanceTransformMaxDist();
void distanceTransformSetSpread();
void distanceTransformInterior();
//stack blur
void stackBlur();
void alphaOnlyBlur();
//flip
void flipHorizontal();
void flipVertical();
private:
QString mReport;
QString mSampleImage;
bool imageCheck( QString testName , QImage &image, int mismatchCount );
};
void TestQgsImageOperation::initTestCase()
{
mReport += "<h1>Image Operation Tests</h1>\n";
mSampleImage = QString( TEST_DATA_DIR ) + QDir::separator() + "sample_image.png";
}
void TestQgsImageOperation::cleanupTestCase()
{
QString myReportFile = QDir::tempPath() + QDir::separator() + "qgistest.html";
QFile myFile( myReportFile );
if ( myFile.open( QIODevice::WriteOnly | QIODevice::Append ) )
{
QTextStream myQTextStream( &myFile );
myQTextStream << mReport;
myFile.close();
}
}
void TestQgsImageOperation::init()
{
}
void TestQgsImageOperation::cleanup()
{
}
void TestQgsImageOperation::smallImageOp()
{
QImage image( QString( TEST_DATA_DIR ) + QDir::separator() + "small_sample_image.png" );
QgsImageOperation::convertToGrayscale( image, QgsImageOperation::GrayscaleLightness );
bool result = imageCheck( QString( "imageop_smallimage" ), image, 0 );
QVERIFY( result );
}
void TestQgsImageOperation::grayscaleLightness()
{
QImage image( mSampleImage );
QgsImageOperation::convertToGrayscale( image, QgsImageOperation::GrayscaleLightness );
bool result = imageCheck( QString( "imageop_graylightness" ), image, 0 );
QVERIFY( result );
}
void TestQgsImageOperation::grayscaleLuminosity()
{
QImage image( mSampleImage );
QgsImageOperation::convertToGrayscale( image, QgsImageOperation::GrayscaleLuminosity );
bool result = imageCheck( QString( "imageop_grayluminosity" ), image, 0 );
QVERIFY( result );
}
void TestQgsImageOperation::grayscaleAverage()
{
QImage image( mSampleImage );
QgsImageOperation::convertToGrayscale( image, QgsImageOperation::GrayscaleAverage );
bool result = imageCheck( QString( "imageop_grayaverage" ), image, 0 );
QVERIFY( result );
}
void TestQgsImageOperation::brightnessContrastNoChange()
{
QImage image( mSampleImage );
QgsImageOperation::adjustBrightnessContrast( image, 0, 1.0 );
bool result = imageCheck( QString( "imageop_bcnochange" ), image, 0 );
QVERIFY( result );
}
void TestQgsImageOperation::increaseBrightness()
{
QImage image( mSampleImage );
QgsImageOperation::adjustBrightnessContrast( image, 50, 1.0 );
bool result = imageCheck( QString( "imageop_increasebright" ), image, 0 );
QVERIFY( result );
}
void TestQgsImageOperation::decreaseBrightness()
{
QImage image( mSampleImage );
QgsImageOperation::adjustBrightnessContrast( image, -50, 1.0 );
bool result = imageCheck( QString( "imageop_decreasebright" ), image, 0 );
QVERIFY( result );
}
void TestQgsImageOperation::increaseContrast()
{
QImage image( mSampleImage );
QgsImageOperation::adjustBrightnessContrast( image, 0, 30.0 );
bool result = imageCheck( QString( "imageop_increasecontrast" ), image, 0 );
QVERIFY( result );
}
void TestQgsImageOperation::decreaseContrast()
{
QImage image( mSampleImage );
QgsImageOperation::adjustBrightnessContrast( image, 0, 0.1 );
bool result = imageCheck( QString( "imageop_decreasecontrast" ), image, 0 );
QVERIFY( result );
}
void TestQgsImageOperation::hueSaturationNoChange()
{
QImage image( mSampleImage );
QgsImageOperation::adjustHueSaturation( image, 1.0 );
bool result = imageCheck( QString( "imageop_satnochange" ), image, 0 );
QVERIFY( result );
}
void TestQgsImageOperation::increaseSaturation()
{
QImage image( mSampleImage );
QgsImageOperation::adjustHueSaturation( image, 5.0 );
bool result = imageCheck( QString( "imageop_increasesat" ), image, 0 );
QVERIFY( result );
}
void TestQgsImageOperation::decreaseSaturation()
{
QImage image( mSampleImage );
QgsImageOperation::adjustHueSaturation( image, 0.5 );
bool result = imageCheck( QString( "imageop_decreasesat" ), image, 0 );
QVERIFY( result );
}
void TestQgsImageOperation::colorizeFull()
{
QImage image( mSampleImage );
QgsImageOperation::adjustHueSaturation( image, 1.0, QColor( 255, 255, 0 ), 1.0 );
bool result = imageCheck( QString( "imageop_colorizefull" ), image, 0 );
QVERIFY( result );
}
void TestQgsImageOperation::colorizePartial()
{
QImage image( mSampleImage );
QgsImageOperation::adjustHueSaturation( image, 1.0, QColor( 255, 255, 0 ), 0.5 );
bool result = imageCheck( QString( "imageop_colorizepartial" ), image, 0 );
QVERIFY( result );
}
void TestQgsImageOperation::opacityNoChange()
{
QImage image( mSampleImage );
QgsImageOperation::multiplyOpacity( image, 1.0 );
bool result = imageCheck( QString( "imageop_opacitynochange" ), image, 0 );
QVERIFY( result );
}
void TestQgsImageOperation::opacityIncrease()
{
QImage image( mSampleImage );
QgsImageOperation::multiplyOpacity( image, 2.0 );
bool result = imageCheck( QString( "imageop_opacityincrease" ), image, 0 );
QVERIFY( result );
}
void TestQgsImageOperation::opacityDecrease()
{
QImage image( mSampleImage );
QgsImageOperation::multiplyOpacity( image, 0.5 );
bool result = imageCheck( QString( "imageop_opacitydecrease" ), image, 0 );
QVERIFY( result );
}
void TestQgsImageOperation::overlayColor()
{
QImage image( mSampleImage );
QgsImageOperation::overlayColor( image, QColor( 0, 255, 255 ) );
bool result = imageCheck( QString( "imageop_overlaycolor" ), image, 0 );
QVERIFY( result );
}
void TestQgsImageOperation::distanceTransformMaxDist()
{
QImage image( mSampleImage );
QgsVectorGradientColorRampV2 ramp;
QgsImageOperation::DistanceTransformProperties props;
props.useMaxDistance = true;
props.ramp = &ramp;
props.shadeExterior = true;
QgsImageOperation::distanceTransform( image, props );
bool result = imageCheck( QString( "imageop_dt_max" ), image, 0 );
QVERIFY( result );
}
void TestQgsImageOperation::distanceTransformSetSpread()
{
QImage image( mSampleImage );
QgsVectorGradientColorRampV2 ramp;
QgsImageOperation::DistanceTransformProperties props;
props.useMaxDistance = false;
props.spread = 10;
props.ramp = &ramp;
props.shadeExterior = true;
QgsImageOperation::distanceTransform( image, props );
bool result = imageCheck( QString( "imageop_dt_spread" ), image, 0 );
QVERIFY( result );
}
void TestQgsImageOperation::distanceTransformInterior()
{
QImage image( mSampleImage );
QgsVectorGradientColorRampV2 ramp;
QgsImageOperation::DistanceTransformProperties props;
props.useMaxDistance = true;
props.ramp = &ramp;
props.shadeExterior = false;
QgsImageOperation::distanceTransform( image, props );
bool result = imageCheck( QString( "imageop_dt_interior" ), image, 0 );
QVERIFY( result );
}
void TestQgsImageOperation::stackBlur()
{
QImage image( mSampleImage );
QgsImageOperation::stackBlur( image, 10 );
bool result = imageCheck( QString( "imageop_stackblur" ), image, 0 );
QVERIFY( result );
}
void TestQgsImageOperation::alphaOnlyBlur()
{
QImage image( QString( TEST_DATA_DIR ) + QDir::separator() + "small_sample_image.png" );
QgsImageOperation::stackBlur( image, 10, true );
bool result = imageCheck( QString( "imageop_stackblur_alphaonly" ), image, 0 );
QVERIFY( result );
}
void TestQgsImageOperation::flipHorizontal()
{
QImage image( mSampleImage );
QgsImageOperation::flipImage( image, QgsImageOperation::FlipHorizontal );
bool result = imageCheck( QString( "imageop_fliphoz" ), image, 0 );
QVERIFY( result );
}
void TestQgsImageOperation::flipVertical()
{
QImage image( mSampleImage );
QgsImageOperation::flipImage( image, QgsImageOperation::FlipVertical );
bool result = imageCheck( QString( "imageop_flipvert" ), image, 0 );
QVERIFY( result );
}
//
// Private helper functions not called directly by CTest
//
bool TestQgsImageOperation::imageCheck( QString testName, QImage &image, int mismatchCount )
{
//draw background
QImage imageWithBackground( image.width(), image.height(), QImage::Format_RGB32 );
QgsRenderChecker::drawBackground( &imageWithBackground );
QPainter painter( &imageWithBackground );
painter.drawImage( 0, 0, image );
painter.end();
mReport += "<h2>" + testName + "</h2>\n";
QString tempDir = QDir::tempPath() + QDir::separator();
QString fileName = tempDir + testName + ".png";
imageWithBackground.save( fileName, "PNG" );
QgsRenderChecker checker;
checker.setControlName( "expected_" + testName );
checker.setRenderedImage( fileName );
checker.setColorTolerance( 1 );
bool resultFlag = checker.compareImages( testName, mismatchCount );
mReport += checker.report();
return resultFlag;
}
QTEST_MAIN( TestQgsImageOperation )
#include "testqgsimageoperation.moc"

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

BIN
tests/testdata/small_sample_image.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB