Skip to content

Commit

Permalink
Merge pull request #5498 from johnhaddon/imageScatter
Browse files Browse the repository at this point in the history
ImageScatter : Add new node for scattering points on images
  • Loading branch information
johnhaddon authored Oct 20, 2023
2 parents dc9fdf8 + df69c4c commit 23df955
Show file tree
Hide file tree
Showing 12 changed files with 856 additions and 3 deletions.
10 changes: 10 additions & 0 deletions Changes.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
1.3.x.x (relative to 1.3.4.0)
=======

Features
--------

- ImageScatter : Added a new node for scattering points across an image, with density controlled by an image channel.

Improvements
------------

Expand All @@ -20,6 +25,11 @@ Fixes
- Fixed bug that prevented editors being destroyed at the right time.
- FileSystemPath : Fixed bug on Windows where paths on an exFAT partition were not considered valid.

API
---

- Sampler : Added `populate()` method, which populates the internal tile cache in parallel, and subsequently allows `sample()` to be called concurrently.

1.3.4.0 (relative to 1.3.3.0)
=======

Expand Down
20 changes: 17 additions & 3 deletions include/GafferImage/Sampler.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,18 @@ namespace GafferImage
/// access via pixel coordinates, dealing with pixels outside
/// the data window by either clamping or returning black.
///
/// The Sampler is sensitive to the Context which is current
/// during its operation, so a sampler should only be used in
/// the context in which it is constructed.
/// By default, the Sampler populates its internal tile cache
/// on demand, only querying tiles as they are needed by `sample()`
/// or `visitPixels()`. This has two implications :
///
/// - It is not safe to call `sample()` or `visitPixels()`
/// from multiple threads concurrently.
/// - `sample()` and `visitPixels()` must be called with the
/// same `Context` that was used to construct the sampler.
///
/// If concurrency is required or it is necessary to change
/// Context while using the sampler, use the `populate()` method
/// to fill the tile cache in advance.
class GAFFERIMAGE_API Sampler
{

Expand All @@ -74,6 +83,11 @@ class GAFFERIMAGE_API Sampler
/// @param boundingMode The method of handling samples that fall outside the data window.
Sampler( const GafferImage::ImagePlug *plug, const std::string &channelName, const Imath::Box2i &sampleWindow, BoundingMode boundingMode = Black );

/// Uses `parallelProcessTiles()` to fill the internal tile cache
/// with all tiles in the sample window. Allows `sample()` and
/// `visitPixels()` to subsequently be called concurrently.
void populate();

/// Samples the channel value at the specified
/// integer pixel coordinate. It is the caller's
/// responsibility to ensure that this point is
Expand Down
94 changes: 94 additions & 0 deletions include/GafferScene/ImageScatter.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
//////////////////////////////////////////////////////////////////////////
//
// Copyright (c) 2023, Cinesite VFX Ltd. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above
// copyright notice, this list of conditions and the following
// disclaimer.
//
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following
// disclaimer in the documentation and/or other materials provided with
// the distribution.
//
// * Neither the name of John Haddon nor the names of
// any other contributors to this software may be used to endorse or
// promote products derived from this software without specific prior
// written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
//////////////////////////////////////////////////////////////////////////

#pragma once

#include "GafferScene/ObjectSource.h"

#include "GafferImage/ImagePlug.h"

namespace GafferScene
{

class GAFFERSCENE_API ImageScatter : public ObjectSource
{

public :

GAFFER_NODE_DECLARE_TYPE( GafferScene::ImageScatter, ImageScatterTypeId, ObjectSource );

explicit ImageScatter( const std::string &name=defaultName<ImageScatter>() );
~ImageScatter() override;

GafferImage::ImagePlug *imagePlug();
const GafferImage::ImagePlug *imagePlug() const;

Gaffer::StringPlug *viewPlug();
const Gaffer::StringPlug *viewPlug() const;

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

Gaffer::StringPlug *densityChannelPlug();
const Gaffer::StringPlug *densityChannelPlug() const;

Gaffer::StringPlug *primitiveVariablesPlug();
const Gaffer::StringPlug *primitiveVariablesPlug() const;

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

Gaffer::StringPlug *widthChannelPlug();
const Gaffer::StringPlug *widthChannelPlug() const;

void affects( const Gaffer::Plug *input, AffectedPlugsContainer &outputs ) const override;

protected :

void hashSource( const Gaffer::Context *context, IECore::MurmurHash &h ) const override;
IECore::ConstObjectPtr computeSource( const Gaffer::Context *context ) const override;

Gaffer::ValuePlug::CachePolicy computeCachePolicy( const Gaffer::ValuePlug *output ) const override;

private :

static size_t g_firstPlugIndex;

};

IE_CORE_DECLAREPTR( ImageScatter )

} // namespace GafferScene
1 change: 1 addition & 0 deletions include/GafferScene/TypeIds.h
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ enum TypeId
MeshSplitTypeId = 110632,
FramingConstraintTypeId = 110633,
MeshNormalsTypeId = 110634,
ImageScatterTypeId = 110635,

PreviewPlaceholderTypeId = 110647,
PreviewGeometryTypeId = 110648,
Expand Down
184 changes: 184 additions & 0 deletions python/GafferSceneTest/ImageScatterTest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
##########################################################################
#
# Copyright (c) 2023, Cinesite VFX Ltd. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above
# copyright notice, this list of conditions and the following
# disclaimer.
#
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided with
# the distribution.
#
# * Neither the name of John Haddon nor the names of
# any other contributors to this software may be used to endorse or
# promote products derived from this software without specific prior
# written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
##########################################################################

import unittest
import imath

import IECore
import IECoreScene

import Gaffer
import GafferImage
import GafferScene
import GafferSceneTest

class ImageScatterTest( GafferSceneTest.SceneTestCase ) :

def testDensity( self ) :

constant = GafferImage.Constant()

scatter = GafferScene.ImageScatter()
scatter["image"].setInput( constant["out"] )

for width in ( 100, 200, 400 ) :
for height in ( 100, 200, 400 ) :
for pixelAspect in ( 0.5, 1.0, 2.0 ) :
for value in ( 0, 0.1, 0.5, 1.0 ) :
for density in ( 0, 0.1, 0.5, 1.0, 2.0 ) :
with self.subTest( width = width, height = height, value = value, density = density, pixelAspect = pixelAspect ) :
constant["format"].setValue( GafferImage.Format( width, height, pixelAspect ) )
constant["color"]["r"].setValue( value )
scatter["density"].setValue( density )
points = scatter["out"].object( "/points" )
expected = width * pixelAspect * height * density * value
self.assertAlmostEqual(
points.numPoints, expected,
delta = expected * 0.07
)

def testMissingChannels( self ) :

constant = GafferImage.Constant()
scatter = GafferScene.ImageScatter()
scatter["image"].setInput( constant["out"] )

scatter["densityChannel"].setValue( "doesNotExist" )

with self.assertRaisesRegex( Gaffer.ProcessException, 'Density channel "doesNotExist" does not exist' ) :
scatter["out"].object( "/points" )

scatter["densityChannel"].setValue( "R" )
scatter["widthChannel"].setValue( "doesNotExist" )

with self.assertRaisesRegex( Gaffer.ProcessException, 'Width channel "doesNotExist" does not exist' ) :
scatter["out"].object( "/points" )

def testPrimitiveVariables( self ) :

ramp = GafferImage.Ramp()
ramp["format"].setValue( GafferImage.Format( 3, 2 ) )
ramp["startPosition"].setValue( imath.V2f( 0.5, 0 ) )
ramp["endPosition"].setValue( imath.V2f( 2.5, 0 ) )
ramp["ramp"]["p0"]["y"]["a"].setValue( 1 ) # Solid alpha
ramp["ramp"]["interpolation"].setValue( Gaffer.SplineDefinitionInterpolation.Linear )

scatter = GafferScene.ImageScatter()
scatter["image"].setInput( ramp["out"] )
scatter["density"].setValue( 10 )
scatter["densityChannel"].setValue( "A" )
scatter["width"].setValue( 2.0 )
scatter["widthChannel"].setValue( "R" )

points = scatter["out"].object( "/points" )
self.assertEqual( set( points.keys() ), { "P", "width" } )

scatter["primitiveVariables"].setValue( "[RGBA]" )

for pixelAspect in ( 1, 2 ) :

with self.subTest( pixelAspect = pixelAspect ) :

ramp["format"]["pixelAspect"].setValue( pixelAspect )

points = scatter["out"].object( "/points" )
self.assertEqual( set( points.keys() ), { "P", "width", "Cs", "A" } )
self.assertIsInstance( points["Cs"].data, IECore.Color3fVectorData )
self.assertEqual( len( points["Cs"].data ), len( points["P"].data ) )
self.assertIsInstance( points["width"].data, IECore.FloatVectorData )
self.assertEqual( len( points["width"].data ), len( points["P"].data ) )

for i, c in enumerate( points["Cs"].data ) :
# Expected spline value
x = points["P"].data[i].x / pixelAspect
x = (x - 0.5) / 2.0
x = max( 0, min( x, 1 ) )
# Should match what we sampled for `Cs`
self.assertAlmostEqual( c[0], x, delta = 0.000001 )
self.assertAlmostEqual( c[1], x, delta = 0.000001 )
self.assertAlmostEqual( c[2], x, delta = 0.000001 )
# And width should be double that
self.assertAlmostEqual( points["width"].data[i], x * 2, delta = 0.000001 )

self.assertEqual(
points["A"].data,
IECore.FloatVectorData( [ 1.0 ] * len( points["P"].data ) )
)

def testWidth( self ) :

constant = GafferImage.Constant()
constant["format"].setValue( GafferImage.Format( 1, 1 ) )
constant["color"].setValue( imath.Color4f( 1, 0.5, 0, 1 ) )

scatter = GafferScene.ImageScatter()
scatter["image"].setInput( constant["out"] )

# If no width channel is specified, we get a constant width
# from the width plug.

self.assertEqual(
scatter["out"].object( "/points" )["width"],
IECoreScene.PrimitiveVariable(
IECoreScene.PrimitiveVariable.Interpolation.Constant,
IECore.FloatData( 1 )
)
)

scatter["width"].setValue( 0.5 )
self.assertEqual(
scatter["out"].object( "/points" )["width"],
IECoreScene.PrimitiveVariable(
IECoreScene.PrimitiveVariable.Interpolation.Constant,
IECore.FloatData( 0.5 )
)
)

# If a width channel is specified, then that should be multiplied
# with the width from the plug.

scatter["widthChannel"].setValue( "G" )
points = scatter["out"].object( "/points" )
self.assertEqual(
points["width"],
IECoreScene.PrimitiveVariable(
IECoreScene.PrimitiveVariable.Interpolation.Vertex,
IECore.FloatVectorData( [ 0.5 * 0.5 ] * points.numPoints )
)
)

if __name__ == "__main__":
unittest.main()
1 change: 1 addition & 0 deletions python/GafferSceneTest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@
from .MeshSplitTest import MeshSplitTest
from .FramingConstraintTest import FramingConstraintTest
from .MeshNormalsTest import MeshNormalsTest
from .ImageScatterTest import ImageScatterTest

from .IECoreScenePreviewTest import *
from .IECoreGLPreviewTest import *
Expand Down
Loading

0 comments on commit 23df955

Please sign in to comment.