Skip to content

Commit

Permalink
Added the new image transform node with sub-pixel filtering.
Browse files Browse the repository at this point in the history
  • Loading branch information
goddardl committed Jun 6, 2013
1 parent 8bb3239 commit 825aa07
Show file tree
Hide file tree
Showing 19 changed files with 524 additions and 103 deletions.
41 changes: 23 additions & 18 deletions include/GafferImage/ImageTransform.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,49 +37,54 @@
#ifndef GAFFERSCENE_IMAGETRANSFORM_H
#define GAFFERSCENE_IMAGETRANSFORM_H

#include "GafferImage/ImageProcessor.h"
#include "Gaffer/Node.h"
#include "Gaffer/Context.h"
#include "GafferImage/FilterPlug.h"
#include "Gaffer/Transform2DPlug.h"
#include "GafferImage/ImageProcessor.h"
#include "Gaffer/DependencyNode.h"

namespace GafferImage
{

class ImageTransform : public ImageProcessor
{
IE_CORE_FORWARDDECLARE( Reformat );

class ImageTransform : public GafferImage::ImageProcessor
{
public :

ImageTransform( const std::string &name=defaultName<ImageTransform>() );
virtual ~ImageTransform();

IE_CORE_DECLARERUNTIMETYPEDEXTENSION( GafferImage::ImageTransform, ImageTransformTypeId, ImageProcessor );

/// Plug accessors.
virtual void affects( const Gaffer::Plug *input, AffectedPlugsContainer &outputs ) const;
virtual void hash( const Gaffer::ValuePlug *output, const Gaffer::Context *context, IECore::MurmurHash &h ) const;
virtual void compute( Gaffer::ValuePlug *output, const Gaffer::Context *context ) const;

Gaffer::Transform2DPlug *transformPlug();
const Gaffer::Transform2DPlug *transformPlug() const;

virtual void affects( const Gaffer::Plug *input, AffectedPlugsContainer &outputs ) const;
virtual bool enabled() const;

protected :

virtual void hashFormatPlug( const GafferImage::ImagePlug *output, const Gaffer::Context *context, IECore::MurmurHash &h ) const;
virtual void hashChannelNamesPlug( const GafferImage::ImagePlug *output, const Gaffer::Context *context, IECore::MurmurHash &h ) const;
virtual void hashDataWindowPlug( const GafferImage::ImagePlug *output, const Gaffer::Context *context, IECore::MurmurHash &h ) const;
virtual void hashChannelDataPlug( const GafferImage::ImagePlug *output, const Gaffer::Context *context, IECore::MurmurHash &h ) const;
bool enabled() const;

protected:

virtual void hashFormatPlug( const GafferImage::ImagePlug *output, const Gaffer::Context *context, IECore::MurmurHash &h ) const {};
virtual void hashDataWindowPlug( const GafferImage::ImagePlug *output, const Gaffer::Context *context, IECore::MurmurHash &h ) const {} ;
virtual void hashChannelNamesPlug( const GafferImage::ImagePlug *output, const Gaffer::Context *context, IECore::MurmurHash &h ) const {};
virtual void hashChannelDataPlug( const GafferImage::ImagePlug *output, const Gaffer::Context *context, IECore::MurmurHash &h ) const {};

virtual GafferImage::Format computeFormat( const Gaffer::Context *context, const ImagePlug *parent ) const;
virtual Imath::Box2i computeDataWindow( const Gaffer::Context *context, const ImagePlug *parent ) const;
virtual IECore::ConstStringVectorDataPtr computeChannelNames( const Gaffer::Context *context, const ImagePlug *parent ) const;
virtual IECore::ConstFloatVectorDataPtr computeChannelData( const std::string &channelName, const Imath::V2i &tileOrigin, const Gaffer::Context *context, const ImagePlug *parent ) const;

private :

// A useful method that returns an axis-aligned box that contains box*m.
Imath::Box2i transformBox( const Imath::M33f &m, const Imath::Box2i &box ) const;

static size_t g_firstChildIndex;

GafferImage::FormatPlug *formatPlug();
const GafferImage::FormatPlug *formatPlug() const;

static size_t g_firstPlugIndex;
};

} // namespace GafferImage
Expand Down
2 changes: 1 addition & 1 deletion include/GafferImage/Reformat.h
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ class Reformat : public ImageProcessor
virtual IECore::ConstFloatVectorDataPtr computeChannelData( const std::string &channelName, const Imath::V2i &tileOrigin, const Gaffer::Context *context, const ImagePlug *parent ) const;

// Computes the output scale factor from the input and output formats.
Imath::V2d scale() const;
Imath::V2f scale() const;

private :

Expand Down
2 changes: 2 additions & 0 deletions include/GafferImage/Sampler.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,13 @@

#include <vector>

#include "IECore/FastFloat.h"
#include "IECore/BoxAlgo.h"
#include "IECore/BoxOps.h"

#include "GafferImage/ImagePlug.h"
#include "GafferImage/Filter.h"
#include "GafferImage/TypeIds.h"

namespace GafferImage
{
Expand Down
9 changes: 8 additions & 1 deletion include/GafferImage/Sampler.inl
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ namespace GafferImage

float Sampler::sample( float x, float y )
{
// Perform an early-out for the box filter.
if ( static_cast<GafferImage::TypeId>( m_filter->typeId() ) == GafferImage::BoxFilterTypeId )
{
return sample( IECore::fastFloatFloor( x ), IECore::fastFloatFloor( y ) );
}

// Otherwise do a filtered lookup.
int tapX = m_filter->tap( x - m_cacheWindow.min.x );
const int width = m_filter->width();
float weightsX[width];
Expand Down Expand Up @@ -99,7 +106,7 @@ float Sampler::sample( int x, int y )
p.x = std::max( std::min( p.x, m_sampleWindow.max.x ), m_sampleWindow.min.x );
p.y = std::max( std::min( p.y, m_sampleWindow.max.y ), m_sampleWindow.min.y );
}

const float *tileData;
Imath::V2i tileOrigin;
Imath::V2i tileIndex;
Expand Down
1 change: 1 addition & 0 deletions include/GafferImage/TypeIds.h
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ enum TypeId
CatmullRomFilterTypeId = 110780,
SincFilterTypeId = 110781,
LanczosFilterTypeId = 110782,
ImageTransformImplementationTypeId = 110783,

LastTypeId = 110849
};
Expand Down
107 changes: 75 additions & 32 deletions python/GafferImageTest/ImageTransformTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,24 @@
class ImageTransformTest( unittest.TestCase ) :

fileName = os.path.expandvars( "$GAFFER_ROOT/python/GafferTest/images/checker.exr" )
path = os.path.expandvars( "$GAFFER_ROOT/python/GafferTest/images/" )

def testIdentityHash( self ) :

r = GafferImage.ImageReader()
r["fileName"].setValue( self.fileName )

t = GafferImage.ImageTransform()
t["in"].setInput( r["out"] )

c = Gaffer.Context()
c["image:channelName"] = "R"
c["image:tileOrigin"] = IECore.V2i( 0 )
with c :
h1 = t["out"].hash();
h2 = r["out"].hash();
self.assertEqual( h1, h2 )

def testScaleHash( self ) :

r = GafferImage.ImageReader()
Expand All @@ -71,39 +88,12 @@ def testDirtyPropagation( self ) :
t["transform"]["scale"].setValue( IECore.V2f( 2., 2. ) );

dirtiedPlugs = set( [ x[0].relativeName( x[0].node() ) for x in cs ] )
self.assertEqual( len( dirtiedPlugs ), 1 )
self.assertEqual( len( dirtiedPlugs ), 4 )
self.assertTrue( "out" in dirtiedPlugs )
self.assertTrue( "out.channelData" in dirtiedPlugs )
self.assertTrue( "out.dataWindow" in dirtiedPlugs )
self.assertTrue( "__scaledFormat" in dirtiedPlugs )

# Check that the hash is the same for all integer translations.
# As the ImagePlug::image() method renders the data window of the plug with tiles
# that have an origin relative to the data window, we can assume that the data
# in a tile with origin (0,0) that has a data window origin of (0,0) is the same as
# the same tile translated to (1, 1) if the data window is also (1, 1).
# Therefore, a translation of 1.5 and 4.5 should produce the same channel data
# but with a translated data window. As a result, the channel data hashes should also be the same.
# We test this by comparing the results of several translations that have identical floating point
# parts but different integer parts. As the ImagePlug::image() method renders the
# data window of the plug with tiles that have an origin relative to the data window, we can assume
# that the data in a tile with origin (0,0) that has a data window origin of (0,0) is the same as
# the same tile translated to (1, 1) if the data window is also (1,1).
def testTranslateHash( self ) :

r = GafferImage.ImageReader()
r["fileName"].setValue( self.fileName )

t = GafferImage.ImageTransform()
t["in"].setInput( r["out"] )

tileSize = GafferImage.ImagePlug.tileSize()
v1 = IECore.V2f( 1.5, 0.0 )
v2 = IECore.V2f( 4.5, 0.0 )
for i in range( 0, 30 ) :
t["transform"]["translate"].setValue( v1 + IECore.V2f( i ) )
h1 = t["out"].channelDataHash( "R", t["out"]["dataWindow"].getValue().min )
t["transform"]["translate"].setValue( v2 + IECore.V2f( i ) )
h2 = t["out"].channelDataHash( "R", t["out"]["dataWindow"].getValue().min )
self.assertEqual( h1, h2 )

def testOutputFormat( self ) :

r = GafferImage.ImageReader()
Expand All @@ -118,7 +108,7 @@ def testOutputFormat( self ) :
c["image:tileOrigin"] = IECore.V2i( 0 )
with c :
self.assertEqual( t["out"]["format"].hash(), r["out"]["format"].hash() )

def testHashPassThrough( self ) :

r = GafferImage.ImageReader()
Expand Down Expand Up @@ -158,6 +148,59 @@ def testDisabled( self ) :
t["enabled"].setValue( False )
self.assertEqual( r["out"].hash(), t["out"].hash() )

def testBoxFilter( self ) :
self.__testFilter( "Box" )

def testBSplineFilter( self ) :
self.__testFilter( "BSpline" )

def testBilinearFilter( self ) :
self.__testFilter( "Bilinear" )

def testCatmullRomFilter( self ) :
self.__testFilter( "CatmullRom" )

def testCubicFilter( self ) :
self.__testFilter( "Cubic" )

def testHermiteFilter( self ) :
self.__testFilter( "Hermite" )

def testLanczosFilter( self ) :
self.__testFilter( "Lanczos" )

def testMitchellFilter( self ) :
self.__testFilter( "Mitchell" )

def testSincFilter( self ) :
self.__testFilter( "Sinc" )

def __testFilter( self, filter ) :

reader = GafferImage.ImageReader()
reader["fileName"].setValue( os.path.join( self.path, "checkerWithNegativeDataWindow.200x150.exr" ) )

t = GafferImage.ImageTransform()
t["transform"]["translate"].setValue( IECore.V2f( 20, -10 ) )
t["transform"]["scale"].setValue( IECore.V2f( .75, 1.1 ) )
t["transform"]["rotate"].setValue( 40 )
t["transform"]["pivot"].setValue( IECore.V2f( 50, 30 ) )
t["in"].setInput( reader["out"] )

expectedOutput = GafferImage.ImageReader()

file = "transformedChecker" + filter + ".200x150.exr"
expectedOutput["fileName"].setValue( os.path.join( self.path, file ) )
t["filter"].setValue( filter )

op = IECore.ImageDiffOp()
res = op(
imageA = expectedOutput["out"].image(),
imageB = t["out"].image()
)

self.assertFalse( res.value )

if __name__ == "__main__":
unittest.main()

Expand Down
Binary file modified python/GafferTest/images/checkerBox.200x150.exr
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading

0 comments on commit 825aa07

Please sign in to comment.