Skip to content

Commit

Permalink
Added exposure and gamma controls to the image viewer.
Browse files Browse the repository at this point in the history
This is achieved by setting a preprocessor containing a grade node with appropriate settings. This does mean that any subclasses which were formerly calling setPreprocessor() to perform a conversion will have to be modified to take advantage of the new behaviour - see ImageViewTest.testDeriving for an example.

The viewer has an outstanding bug (which already had a todo) whereby the sampled colour at the mouse x,y coordinates includes the additional transforms, whereas it should not. I haven't addressed this here, but instead have added todo items describing the approach I think we should take.

Fixes GafferHQ#571.
  • Loading branch information
johnhaddon committed Nov 8, 2013
1 parent c9fabd5 commit 8b90c54
Show file tree
Hide file tree
Showing 5 changed files with 217 additions and 36 deletions.
53 changes: 40 additions & 13 deletions include/GafferImageUI/ImageView.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,21 @@
#ifndef GAFFERIMAGEUI_IMAGEVIEW_H
#define GAFFERIMAGEUI_IMAGEVIEW_H

#include "GafferUI/View.h"
#include "Gaffer/NumericPlug.h"

#include "GafferImage/ImagePlug.h"
#include "GafferImage/ImageStats.h"
#include "GafferUI/View.h"

#include "GafferImageUI/TypeIds.h"
#include "GafferImageUIBindings/ImageViewBinding.h" // to enable friend declaration for bindImageView().

namespace GafferImage
{

IE_CORE_FORWARDDECLARE( Grade )
IE_CORE_FORWARDDECLARE( ImageStats )
IE_CORE_FORWARDDECLARE( ImagePlug )

} // namespace GafferImage

namespace GafferImageUI
{
Expand All @@ -58,29 +67,47 @@ class ImageView : public GafferUI::View

IE_CORE_DECLARERUNTIMETYPEDEXTENSION( GafferImageUI::ImageView, ImageViewTypeId, GafferUI::View );

protected :

/// This constructor is for classes which derive from ImageView, but
/// don't necessarily accept an ImagePlug. Instead they should create
/// a preprocessor node that accepts the input plug and outputs an
/// image plug, and enable it using setPreprocessor().
ImageView( const std::string &name, Gaffer::PlugPtr input );
Gaffer::FloatPlug *exposurePlug();
const Gaffer::FloatPlug *exposurePlug() const;

Gaffer::FloatPlug *gammaPlug();
const Gaffer::FloatPlug *gammaPlug() const;

protected :

/// May be called from a subclass constructor to add a converter
/// from non-image input types, allowing them to be viewed as images.
/// The converter must have an "in" Plug (of any desired type), and
/// convert the incoming data to an image to view on an "out" ImagePlug.
/// \note If the necessary conversion requires several nodes, a Box
/// provides a means of packaging them to meet these requirements.
/// \note Subclasses are not allowed to call setProcessor() as the
/// preprocessor is managed by the ImageView base class.
void insertConverter( Gaffer::NodePtr converter );

virtual void update();

private:

GafferImage::ImageStats *imageStatsNode();
const GafferImage::ImageStats *imageStatsNode() const;

static ViewDescription<ImageView> g_viewDescription;

private:
GafferImage::Grade *gradeNode();
const GafferImage::Grade *gradeNode() const;

void plugSet( Gaffer::Plug *plug );

int m_channelToView;
Imath::V2f m_mousePos;
Imath::Color4f m_sampleColor;
Imath::Color4f m_minColor;
Imath::Color4f m_maxColor;
Imath::Color4f m_averageColor;

static ViewDescription<ImageView> g_viewDescription;

friend void GafferImageUIBindings::bindImageView();

};

IE_CORE_DECLAREPTR( ImageView );
Expand Down
4 changes: 2 additions & 2 deletions include/GafferUI/View.inl
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,13 @@ namespace GafferUI
template<typename T>
T *View::inPlug()
{
return getChild<T>( g_firstPlugIndex );
return getChild<T>( "in" );
}

template<typename T>
const T *View::inPlug() const
{
return getChild<T>( g_firstPlugIndex );
return getChild<T>( "in" );
}

template<typename T>
Expand Down
36 changes: 35 additions & 1 deletion python/GafferImageUITest/ImageViewTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def testFactory( self ) :
self.assertTrue( isinstance( view, GafferImageUI.ImageView ) )
self.assertTrue( view["in"].getInput().isSame( image["out"] ) )

def testDeriving( self ) :
def testDeprecatedDeriving( self ) :

class MyView( GafferImageUI.ImageView ) :

Expand All @@ -82,9 +82,43 @@ def __init__( self, viewedPlug = None ) :
self.assertTrue( isinstance( view, MyView ) )
self.assertTrue( view["in"].getInput().isSame( sphere["out"] ) )
self.assertTrue( isinstance( view["in"], Gaffer.ObjectPlug ) )
view["exposure"].setValue( 1 )
view["gamma"].setValue( 0.5 )

view._update()

def testDeriving( self ) :

class MyView( GafferImageUI.ImageView ) :

def __init__( self, viewedPlug = None ) :

GafferImageUI.ImageView.__init__( self, "MyView" )

converter = Gaffer.Node()
converter["in"] = Gaffer.ObjectPlug( defaultValue = IECore.NullObject.defaultNullObject() )
converter["out"] = GafferImage.ImagePlug( direction = Gaffer.Plug.Direction.Out )
converter["constant"] = GafferImage.Constant()
converter["constant"]["format"].setValue( GafferImage.Format( 20, 20, 1 ) )
converter["out"].setInput( converter["constant"]["out"] )

self._insertConverter( converter )

self["in"].setInput( viewedPlug )

GafferUI.View.registerView( GafferTest.SphereNode.staticTypeId(), "out", MyView )

sphere = GafferTest.SphereNode()

view = GafferUI.View.create( sphere["out"] )
self.assertTrue( isinstance( view, MyView ) )
self.assertTrue( view["in"].getInput().isSame( sphere["out"] ) )
self.assertTrue( isinstance( view["in"], Gaffer.ObjectPlug ) )
view["exposure"].setValue( 1 )
view["gamma"].setValue( 0.5 )

view._update()

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

157 changes: 137 additions & 20 deletions src/GafferImageUI/ImageView.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@
#include "GafferUI/Pointer.h"

#include "GafferImage/Format.h"
#include "GafferImage/Grade.h"
#include "GafferImage/ImagePlug.h"
#include "GafferImage/ImageStats.h"

#include "GafferImageUI/ImageView.h"

Expand All @@ -80,6 +83,14 @@ namespace GafferImageUI
namespace Detail
{

/// \todo Refactor all the colour sampling and swatch drawing out of here.
/// Sampled colours should be available on the ImageView as output plugs,
/// and a new FooterToolbar type thing in the Viewer should be used for drawing
/// them using the standard Widget classes. The ImageViewGadget itself should be
/// responsible only for the setting of the input plugs on the samplers during
/// mouse move and drag and drop operations. This way the Widgets will give us all
/// the colour correction (using GafferUI.DisplayTransform), text selection and drag
/// and drop we could desire for free.
class ImageViewGadget : public GafferUI::Gadget
{

Expand Down Expand Up @@ -909,42 +920,129 @@ ImageView::ViewDescription<ImageView> ImageView::g_viewDescription( GafferImage:

ImageView::ImageView( const std::string &name )
: View( name, new GafferImage::ImagePlug() ),
m_channelToView(0),
m_mousePos(0.),
m_sampleColor(0.),
m_minColor(0.),
m_maxColor(0.),
m_averageColor(0.)
m_channelToView( 0 ),
m_mousePos( Imath::V2f( 0.0f ) ),
m_sampleColor( Imath::Color4f( 0.0f ) ),
m_minColor( Imath::Color4f( 0.0f ) ),
m_maxColor( Imath::Color4f( 0.0f ) ),
m_averageColor( Imath::Color4f( 0.0f ) )
{
// Create an internal ImageStats node
addChild( new GafferImage::ImageStats( "imageStats" ) );

// build the preprocessor we use for applying colour
// transforms, and the stats node we use for displaying stats.

NodePtr preprocessor = new Node;
ImagePlugPtr preprocessorInput = new ImagePlug( "in" );
preprocessor->addChild( preprocessorInput );

ImageStatsPtr statsNode = new ImageStats( "__imageStats" );
addChild( statsNode ); /// \todo Store this in the preprocessor when we've disallowed the changing of it by subclasses
statsNode->inPlug()->setInput( preprocessorInput );
statsNode->channelsPlug()->setInput( preprocessorInput->channelNamesPlug() );

GradePtr gradeNode = new Grade;
preprocessor->setChild( "__grade", gradeNode );
gradeNode->inPlug()->setInput( preprocessorInput );

FloatPlugPtr exposurePlug = new FloatPlug( "exposure" );
exposurePlug->setFlags( Plug::AcceptsInputs, false );
addChild( exposurePlug ); // dealt with in plugSet()

PlugPtr gammaPlug = gradeNode->gammaPlug()->getChild( 0 )->createCounterpart( "gamma", Plug::In );
gammaPlug->setFlags( Plug::AcceptsInputs, false );
addChild( gammaPlug );
gradeNode->gammaPlug()->getChild( 0 )->setInput( gammaPlug );
gradeNode->gammaPlug()->getChild( 1 )->setInput( gammaPlug );
gradeNode->gammaPlug()->getChild( 2 )->setInput( gammaPlug );

ImagePlugPtr preprocessorOutput = new ImagePlug( "out", Plug::Out );
preprocessor->addChild( preprocessorOutput );
preprocessorOutput->setInput( gradeNode->outPlug() );

// tell the base class about all the preprocessing we want to do

setPreprocessor( preprocessor );

// connect up to some signals

plugSetSignal().connect( boost::bind( &ImageView::plugSet, this, ::_1 ) );

}

ImageView::ImageView( const std::string &name, Gaffer::PlugPtr input )
: View( name, input ),
m_channelToView(0),
m_mousePos( 0. ),
m_sampleColor(0.),
m_minColor(0.),
m_maxColor(0.),
m_averageColor(0.)
void ImageView::insertConverter( Gaffer::NodePtr converter )
{
// Create an internal ImageStats node
addChild( new GafferImage::ImageStats( "imageStats" ) );
PlugPtr converterInput = converter->getChild<Plug>( "in" );
if( !converterInput )
{
throw IECore::Exception( "Converter has no Plug named \"in\"" );
}
ImagePlugPtr converterOutput = converter->getChild<ImagePlug>( "out" );
if( !converterOutput )
{
throw IECore::Exception( "Converter has no ImagePlug named \"out\"" );
}

PlugPtr newInput = converterInput->createCounterpart( "in", Plug::In );
setChild( "in", newInput );

NodePtr preprocessor = getPreprocessor<Node>();
Plug::OutputContainer outputsToRestore = preprocessor->getChild<ImagePlug>( "in" )->outputs();

PlugPtr newPreprocessorInput = converterInput->createCounterpart( "in", Plug::In );
preprocessor->setChild( "in", newPreprocessorInput );
newPreprocessorInput->setInput( newInput );

preprocessor->setChild( "__converter", converter );
converterInput->setInput( newPreprocessorInput );

for( Plug::OutputContainer::const_iterator it = outputsToRestore.begin(), eIt = outputsToRestore.end(); it != eIt; ++it )
{
(*it)->setInput( converterOutput );
}
}

ImageView::~ImageView()
{
}

Gaffer::FloatPlug *ImageView::exposurePlug()
{
return getChild<FloatPlug>( "exposure" );
}

const Gaffer::FloatPlug *ImageView::exposurePlug() const
{
return getChild<FloatPlug>( "exposure" );
}

Gaffer::FloatPlug *ImageView::gammaPlug()
{
return getChild<FloatPlug>( "gamma" );
}

const Gaffer::FloatPlug *ImageView::gammaPlug() const
{
return getChild<FloatPlug>( "gamma" );
}

GafferImage::ImageStats *ImageView::imageStatsNode()
{
return getChild<ImageStats>( "imageStats" );
return getChild<ImageStats>( "__imageStats" );
}

const GafferImage::ImageStats *ImageView::imageStatsNode() const
{
return getChild<ImageStats>( "imageStats" );
return getChild<ImageStats>( "__imageStats" );
}

GafferImage::Grade *ImageView::gradeNode()
{
return getPreprocessor<Node>()->getChild<Grade>( "__grade" );
}

const GafferImage::Grade *ImageView::gradeNode() const
{
return getPreprocessor<Node>()->getChild<Grade>( "__grade" );
}

void ImageView::update()
Expand All @@ -968,6 +1066,8 @@ void ImageView::update()
throw IECore::Exception("ImageView: Failed to find an input ImagePlug");
}

/// \todo We should be able to remove this when we remove the python binding
/// which takes an input plug and allows a derived class to call setPreprocessor itself.
imageStatsNode()->inPlug()->setInput( imagePlug );
imageStatsNode()->channelsPlug()->setInput( imagePlug->channelNamesPlug() );

Expand All @@ -985,3 +1085,20 @@ void ImageView::update()
}
}

void ImageView::plugSet( Gaffer::Plug *plug )
{
if( plug == exposurePlug() )
{
Grade *g = gradeNode();
// we have to guard against g not existing until we've removed
// the deprecated constructor, after which no subclasses should
// be calling setPreprocessor().
/// \todo Remove guard when removing constructor
if( g )
{
const float m = pow( 2.0f, exposurePlug()->getValue() );
g->multiplyPlug()->setValue( Color3f( m ) );
}
}
}

3 changes: 3 additions & 0 deletions src/GafferImageUIBindings/ImageViewBinding.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ class ImageViewWrapper : public NodeWrapper<ImageView>

public :

/// \todo Remove deprecated input argument.
ImageViewWrapper( PyObject *self, const std::string &name, Gaffer::PlugPtr input = 0 )
: NodeWrapper<ImageView>( self, name )
{
Expand All @@ -68,7 +69,9 @@ void GafferImageUIBindings::bindImageView()
{

GafferBindings::NodeClass<ImageView, ImageViewWrapperPtr>()
/// \todo Remove deprecated input argument.
.def( init<const std::string &, Gaffer::PlugPtr>() )
.def( "_insertConverter", &ImageView::insertConverter )
;

}

0 comments on commit 8b90c54

Please sign in to comment.