Skip to content

Commit 5c3198d

Browse files
committed
Implement a photoshop-esque curve transformer
Handles scaling of input values to output values by using a curve created from smoothly joining a number of set control points. This assists in creation of curve type transforms, typically seen in raster image editing software (eg the curves dialog in GIMP or Photoshop). Transforms are created by passing a number of set control points through which the transform curve must pass. The curve is guaranteed to exactly pass through these control points. Between control points the curve is smoothly interpolated so that no disjoint sections or "corners" are present.
1 parent 66888fe commit 5c3198d

File tree

4 files changed

+603
-0
lines changed

4 files changed

+603
-0
lines changed

python/core/qgspropertytransformer.sip

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,35 @@
1+
class QgsCurveTransform
2+
{
3+
%TypeHeaderCode
4+
#include <qgspropertytransformer.h>
5+
%End
6+
7+
public:
8+
9+
QgsCurveTransform();
10+
QgsCurveTransform( const QList< QgsPoint >& controlPoints );
11+
~QgsCurveTransform();
12+
QgsCurveTransform( const QgsCurveTransform& other );
13+
14+
//QgsCurveTransform& operator=( const QgsCurveTransform& other );
15+
16+
QList< QgsPoint > controlPoints() const;
17+
18+
void setControlPoints( const QList< QgsPoint >& points );
19+
20+
void addControlPoint( double x, double y );
21+
22+
void removeControlPoint( double x, double y );
23+
24+
double y( double x ) const;
25+
26+
QVector< double > y( const QVector< double >& x ) const;
27+
28+
bool readXml( const QDomElement& elem, const QDomDocument& doc );
29+
30+
bool writeXml( QDomElement& transformElem, QDomDocument& doc ) const;
31+
32+
};
133
class QgsPropertyTransformer
234
{
335
%TypeHeaderCode

src/core/qgspropertytransformer.cpp

Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,3 +586,334 @@ void QgsColorRampTransformer::setColorRamp( QgsColorRamp* ramp )
586586
{
587587
mGradientRamp.reset( ramp );
588588
}
589+
590+
591+
//
592+
// QgsCurveTransform
593+
//
594+
595+
bool sortByX( const QgsPoint& a, const QgsPoint& b )
596+
{
597+
return a.x() < b.x();
598+
}
599+
600+
QgsCurveTransform::QgsCurveTransform()
601+
{
602+
mControlPoints << QgsPoint( 0, 0 ) << QgsPoint( 1, 1 );
603+
calcSecondDerivativeArray();
604+
}
605+
606+
QgsCurveTransform::QgsCurveTransform( const QList<QgsPoint>& controlPoints )
607+
: mControlPoints( controlPoints )
608+
{
609+
std::sort( mControlPoints.begin(), mControlPoints.end(), sortByX );
610+
calcSecondDerivativeArray();
611+
}
612+
613+
QgsCurveTransform::~QgsCurveTransform()
614+
{
615+
delete [] mSecondDerivativeArray;
616+
}
617+
618+
QgsCurveTransform::QgsCurveTransform( const QgsCurveTransform& other )
619+
: mControlPoints( other.mControlPoints )
620+
{
621+
if ( other.mSecondDerivativeArray )
622+
{
623+
mSecondDerivativeArray = new double[ mControlPoints.count()];
624+
memcpy( mSecondDerivativeArray, other.mSecondDerivativeArray, sizeof( double ) * mControlPoints.count() );
625+
}
626+
}
627+
628+
QgsCurveTransform& QgsCurveTransform::operator=( const QgsCurveTransform & other )
629+
{
630+
mControlPoints = other.mControlPoints;
631+
if ( other.mSecondDerivativeArray )
632+
{
633+
delete [] mSecondDerivativeArray;
634+
mSecondDerivativeArray = new double[ mControlPoints.count()];
635+
memcpy( mSecondDerivativeArray, other.mSecondDerivativeArray, sizeof( double ) * mControlPoints.count() );
636+
}
637+
return *this;
638+
}
639+
640+
void QgsCurveTransform::setControlPoints( const QList<QgsPoint>& points )
641+
{
642+
mControlPoints = points;
643+
std::sort( mControlPoints.begin(), mControlPoints.end(), sortByX );
644+
calcSecondDerivativeArray();
645+
}
646+
647+
void QgsCurveTransform::addControlPoint( double x, double y )
648+
{
649+
QgsPoint point( x, y );
650+
if ( mControlPoints.contains( point ) )
651+
return;
652+
653+
mControlPoints << point;
654+
std::sort( mControlPoints.begin(), mControlPoints.end(), sortByX );
655+
calcSecondDerivativeArray();
656+
}
657+
658+
void QgsCurveTransform::removeControlPoint( double x, double y )
659+
{
660+
for ( int i = 0; i < mControlPoints.count(); ++i )
661+
{
662+
if ( qgsDoubleNear( mControlPoints.at( i ).x(), x )
663+
&& qgsDoubleNear( mControlPoints.at( i ).y(), y ) )
664+
{
665+
mControlPoints.removeAt( i );
666+
break;
667+
}
668+
}
669+
calcSecondDerivativeArray();
670+
}
671+
672+
// this code is adapted from https://github.com/OpenFibers/Photoshop-Curves
673+
// which in turn was adapted from
674+
// http://www.developpez.net/forums/d331608-3/autres-langages/algorithmes/contribuez/image-interpolation-spline-cubique/#post3513925 //#spellok
675+
676+
double QgsCurveTransform::y( double x ) const
677+
{
678+
int n = mControlPoints.count();
679+
if ( n < 2 )
680+
return x; // invalid
681+
else if ( n < 3 )
682+
{
683+
// linear
684+
if ( x <= mControlPoints.at( 0 ).x() )
685+
return mControlPoints.at( 0 ).y();
686+
else if ( x >= mControlPoints.at( n - 1 ).x() )
687+
return mControlPoints.at( 1 ).y();
688+
else
689+
{
690+
double dx = mControlPoints.at( 1 ).x() - mControlPoints.at( 0 ).x();
691+
double dy = mControlPoints.at( 1 ).y() - mControlPoints.at( 0 ).y();
692+
return x * ( dy / dx ) + mControlPoints.at( 0 ).y();
693+
}
694+
}
695+
696+
// safety check
697+
if ( x <= mControlPoints.at( 0 ).x() )
698+
return mControlPoints.at( 0 ).y();
699+
if ( x >= mControlPoints.at( n - 1 ).x() )
700+
return mControlPoints.at( n - 1 ).y();
701+
702+
// find corresponding segment
703+
QList<QgsPoint>::const_iterator pointIt = mControlPoints.constBegin();
704+
QgsPoint currentControlPoint = *pointIt;
705+
++pointIt;
706+
QgsPoint nextControlPoint = *pointIt;
707+
708+
for ( int i = 0; i < n - 1; ++i )
709+
{
710+
if ( x < nextControlPoint.x() )
711+
{
712+
// found segment
713+
double h = nextControlPoint.x() - currentControlPoint.x();
714+
double t = ( x - currentControlPoint.x() ) / h;
715+
716+
double a = 1 - t;
717+
718+
return a*currentControlPoint.y() + t*nextControlPoint.y() + ( h*h / 6 )*(( a*a*a - a )*mSecondDerivativeArray[i] + ( t*t*t - t )*mSecondDerivativeArray[i+1] );
719+
}
720+
721+
++pointIt;
722+
if ( pointIt == mControlPoints.constEnd() )
723+
break;
724+
725+
currentControlPoint = nextControlPoint;
726+
nextControlPoint = *pointIt;
727+
}
728+
729+
//should not happen
730+
return x;
731+
}
732+
733+
// this code is adapted from https://github.com/OpenFibers/Photoshop-Curves
734+
// which in turn was adapted from
735+
// http://www.developpez.net/forums/d331608-3/autres-langages/algorithmes/contribuez/image-interpolation-spline-cubique/#post3513925 //#spellok
736+
737+
QVector<double> QgsCurveTransform::y( const QVector<double>& x ) const
738+
{
739+
QVector<double> result;
740+
741+
int n = mControlPoints.count();
742+
if ( n < 3 )
743+
{
744+
// invalid control points - use simple transform
745+
Q_FOREACH ( double i, x )
746+
result << y( i );
747+
748+
return result;
749+
}
750+
751+
// find corresponding segment
752+
QList<QgsPoint>::const_iterator pointIt = mControlPoints.constBegin();
753+
QgsPoint currentControlPoint = *pointIt;
754+
++pointIt;
755+
QgsPoint nextControlPoint = *pointIt;
756+
757+
int xIndex = 0;
758+
double currentX = x.at( xIndex );
759+
// safety check
760+
while ( currentX <= currentControlPoint.x() )
761+
{
762+
result << currentControlPoint.y();
763+
xIndex++;
764+
currentX = x.at( xIndex );
765+
}
766+
767+
for ( int i = 0; i < n - 1; ++i )
768+
{
769+
while ( currentX < nextControlPoint.x() )
770+
{
771+
// found segment
772+
double h = nextControlPoint.x() - currentControlPoint.x();
773+
774+
double t = ( currentX - currentControlPoint.x() ) / h;
775+
776+
double a = 1 - t;
777+
778+
result << a*currentControlPoint.y() + t*nextControlPoint.y() + ( h*h / 6 )*(( a*a*a - a )*mSecondDerivativeArray[i] + ( t*t*t - t )*mSecondDerivativeArray[i+1] );
779+
xIndex++;
780+
if ( xIndex == x.count() )
781+
return result;
782+
783+
currentX = x.at( xIndex );
784+
}
785+
786+
++pointIt;
787+
if ( pointIt == mControlPoints.constEnd() )
788+
break;
789+
790+
currentControlPoint = nextControlPoint;
791+
nextControlPoint = *pointIt;
792+
}
793+
794+
// safety check
795+
while ( xIndex < x.count() )
796+
{
797+
result << nextControlPoint.y();
798+
xIndex++;
799+
}
800+
801+
return result;
802+
}
803+
804+
bool QgsCurveTransform::readXml( const QDomElement& elem, const QDomDocument& )
805+
{
806+
QString xString = elem.attribute( QStringLiteral( "x" ) );
807+
QString yString = elem.attribute( QStringLiteral( "y" ) );
808+
809+
QStringList xVals = xString.split( ',' );
810+
QStringList yVals = yString.split( ',' );
811+
if ( xVals.count() != yVals.count() )
812+
return false;
813+
814+
QList< QgsPoint > newPoints;
815+
bool ok = false;
816+
for ( int i = 0; i < xVals.count(); ++i )
817+
{
818+
double x = xVals.at( i ).toDouble( &ok );
819+
if ( !ok )
820+
return false;
821+
double y = yVals.at( i ).toDouble( &ok );
822+
if ( !ok )
823+
return false;
824+
newPoints << QgsPoint( x, y );
825+
}
826+
setControlPoints( newPoints );
827+
return true;
828+
}
829+
830+
bool QgsCurveTransform::writeXml( QDomElement& transformElem, QDomDocument& ) const
831+
{
832+
QStringList x;
833+
QStringList y;
834+
Q_FOREACH ( const QgsPoint& p, mControlPoints )
835+
{
836+
x << qgsDoubleToString( p.x() );
837+
y << qgsDoubleToString( p.y() );
838+
}
839+
840+
transformElem.setAttribute( QStringLiteral( "x" ), x.join( ',' ) );
841+
transformElem.setAttribute( QStringLiteral( "y" ), y.join( ',' ) );
842+
843+
return true;
844+
}
845+
846+
// this code is adapted from https://github.com/OpenFibers/Photoshop-Curves
847+
// which in turn was adapted from
848+
// http://www.developpez.net/forums/d331608-3/autres-langages/algorithmes/contribuez/image-interpolation-spline-cubique/#post3513925 //#spellok
849+
850+
void QgsCurveTransform::calcSecondDerivativeArray()
851+
{
852+
int n = mControlPoints.count();
853+
if ( n < 3 )
854+
return; // cannot proceed
855+
856+
delete[] mSecondDerivativeArray;
857+
858+
double* matrix = new double[ n * 3 ];
859+
double* result = new double[ n ];
860+
matrix[0] = 0;
861+
matrix[1] = 1;
862+
matrix[2] = 0;
863+
result[0] = 0;
864+
QList<QgsPoint>::const_iterator pointIt = mControlPoints.constBegin();
865+
QgsPoint pointIm1 = *pointIt;
866+
++pointIt;
867+
QgsPoint pointI = *pointIt;
868+
++pointIt;
869+
QgsPoint pointIp1 = *pointIt;
870+
871+
for ( int i = 1; i < n - 1; ++i )
872+
{
873+
matrix[i * 3 + 0 ] = ( pointI.x() - pointIm1.x() ) / 6.0;
874+
matrix[i * 3 + 1 ] = ( pointIp1.x() - pointIm1.x() ) / 3.0;
875+
matrix[i * 3 + 2 ] = ( pointIp1.x() - pointI.x() ) / 6.0;
876+
result[i] = ( pointIp1.y() - pointI.y() ) / ( pointIp1.x() - pointI.x() ) - ( pointI.y() - pointIm1.y() ) / ( pointI.x() - pointIm1.x() );
877+
878+
// shuffle points
879+
pointIm1 = pointI;
880+
pointI = pointIp1;
881+
++pointIt;
882+
if ( pointIt == mControlPoints.constEnd() )
883+
break;
884+
885+
pointIp1 = *pointIt;
886+
}
887+
matrix[( n-1 )*3 + 0] = 0;
888+
matrix[( n-1 )*3 + 1] = 1;
889+
matrix[( n-1 ) * 3 +2] = 0;
890+
result[n-1] = 0;
891+
892+
// solving pass1 (up->down)
893+
for ( int i = 1; i < n; ++i )
894+
{
895+
double k = matrix[i * 3 + 0] / matrix[( i-1 ) * 3 + 1];
896+
matrix[i * 3 + 1] -= k * matrix[( i-1 )*3+2];
897+
matrix[i * 3 + 0] = 0;
898+
result[i] -= k * result[i-1];
899+
}
900+
// solving pass2 (down->up)
901+
for ( int i = n - 2; i >= 0; --i )
902+
{
903+
double k = matrix[i*3+2] / matrix[( i+1 )*3+1];
904+
matrix[i*3+1] -= k * matrix[( i+1 )*3+0];
905+
matrix[i*3+2] = 0;
906+
result[i] -= k * result[i+1];
907+
}
908+
909+
// return second derivative value for each point
910+
mSecondDerivativeArray = new double[n];
911+
for ( int i = 0;i < n;++i )
912+
{
913+
mSecondDerivativeArray[i] = result[i] / matrix[( i*3 )+1];
914+
}
915+
916+
delete[] result;
917+
delete[] matrix;
918+
}
919+

0 commit comments

Comments
 (0)