From a836b83cad3bfdf6d43d4774761552f75db200ab Mon Sep 17 00:00:00 2001 From: John Haddon Date: Fri, 13 Oct 2023 15:35:14 +0100 Subject: [PATCH 1/3] Sampler : Add `populate()` method --- Changes.md | 5 +++++ include/GafferImage/Sampler.h | 20 +++++++++++++++++--- src/GafferImage/Sampler.cpp | 16 ++++++++++++++++ 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/Changes.md b/Changes.md index aa935d5aad2..592504cf306 100644 --- a/Changes.md +++ b/Changes.md @@ -8,6 +8,11 @@ Improvements - Popups for string cells and row names are now sized to fit their column. - Added "Triple" and "Quadruple" width options to the spreadsheet row name popup menu. +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) ======= diff --git a/include/GafferImage/Sampler.h b/include/GafferImage/Sampler.h index 382483b8548..8ac92cf17f5 100644 --- a/include/GafferImage/Sampler.h +++ b/include/GafferImage/Sampler.h @@ -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 { @@ -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 diff --git a/src/GafferImage/Sampler.cpp b/src/GafferImage/Sampler.cpp index c7efc09ec15..47fc7be11c0 100644 --- a/src/GafferImage/Sampler.cpp +++ b/src/GafferImage/Sampler.cpp @@ -36,6 +36,8 @@ #include "GafferImage/Sampler.h" +#include "GafferImage/ImageAlgo.h" + using namespace IECore; using namespace Imath; using namespace Gaffer; @@ -113,6 +115,20 @@ Sampler::Sampler( const GafferImage::ImagePlug *plug, const std::string &channel m_cacheOriginIndex = ( m_cacheWindow.min.x >> ImagePlug::tileSizeLog2() ) + m_cacheWidth * ( m_cacheWindow.min.y >> ImagePlug::tileSizeLog2() ); } +void Sampler::populate() +{ + ImageAlgo::parallelProcessTiles( + m_plug, + [&] ( const ImagePlug *imagePlug, const V2i &tileOrigin ) { + const float *tileData; + int tilePixelIndex; + cachedData( tileOrigin, tileData, tilePixelIndex ); + assert( tilePixelIndex == 0 ); + }, + m_cacheWindow + ); +} + void Sampler::hash( IECore::MurmurHash &h ) const { for ( int x = m_cacheWindow.min.x; x < m_cacheWindow.max.x; x += GafferImage::ImagePlug::tileSize() ) From 63ad7e896e97984c6eee2200a7b0ef9c25a71b81 Mon Sep 17 00:00:00 2001 From: John Haddon Date: Fri, 13 Oct 2023 16:17:32 +0100 Subject: [PATCH 2/3] ImageScatter : Add new node for scattering points on images --- Changes.md | 5 + include/GafferScene/ImageScatter.h | 94 +++++ include/GafferScene/TypeIds.h | 1 + python/GafferSceneTest/ImageScatterTest.py | 177 ++++++++++ python/GafferSceneTest/__init__.py | 1 + python/GafferSceneUI/ImageScatterUI.py | 158 +++++++++ python/GafferSceneUI/__init__.py | 1 + src/GafferScene/ImageScatter.cpp | 371 ++++++++++++++++++++ src/GafferSceneModule/PrimitivesBinding.cpp | 2 + startup/gui/menus.py | 1 + 10 files changed, 811 insertions(+) create mode 100644 include/GafferScene/ImageScatter.h create mode 100644 python/GafferSceneTest/ImageScatterTest.py create mode 100644 python/GafferSceneUI/ImageScatterUI.py create mode 100644 src/GafferScene/ImageScatter.cpp diff --git a/Changes.md b/Changes.md index 592504cf306..4844da90647 100644 --- a/Changes.md +++ b/Changes.md @@ -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 ------------ diff --git a/include/GafferScene/ImageScatter.h b/include/GafferScene/ImageScatter.h new file mode 100644 index 00000000000..1adf3abbf7c --- /dev/null +++ b/include/GafferScene/ImageScatter.h @@ -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() 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 diff --git a/include/GafferScene/TypeIds.h b/include/GafferScene/TypeIds.h index 8700fe93e1f..7826449213c 100644 --- a/include/GafferScene/TypeIds.h +++ b/include/GafferScene/TypeIds.h @@ -176,6 +176,7 @@ enum TypeId MeshSplitTypeId = 110632, FramingConstraintTypeId = 110633, MeshNormalsTypeId = 110634, + ImageScatterTypeId = 110635, PreviewPlaceholderTypeId = 110647, PreviewGeometryTypeId = 110648, diff --git a/python/GafferSceneTest/ImageScatterTest.py b/python/GafferSceneTest/ImageScatterTest.py new file mode 100644 index 00000000000..3ac759ed404 --- /dev/null +++ b/python/GafferSceneTest/ImageScatterTest.py @@ -0,0 +1,177 @@ +########################################################################## +# +# 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( 2, 3 ) ) + ramp["startPosition"].setValue( imath.V2f( 0, 0.5 ) ) + ramp["endPosition"].setValue( imath.V2f( 0, 2.5 ) ) + 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]" ) + 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 + y = points["P"].data[i].y + y = (y - 0.5) / 2.0 + y = max( 0, min( y, 1 ) ) + # Should match what we sampled for `Cs` + self.assertAlmostEqual( c[0], y, delta = 0.000001 ) + self.assertAlmostEqual( c[1], y, delta = 0.000001 ) + self.assertAlmostEqual( c[2], y, delta = 0.000001 ) + # And width should be double that + self.assertAlmostEqual( points["width"].data[i], y * 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() diff --git a/python/GafferSceneTest/__init__.py b/python/GafferSceneTest/__init__.py index 32994629f11..c3819ea5293 100644 --- a/python/GafferSceneTest/__init__.py +++ b/python/GafferSceneTest/__init__.py @@ -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 * diff --git a/python/GafferSceneUI/ImageScatterUI.py b/python/GafferSceneUI/ImageScatterUI.py new file mode 100644 index 00000000000..1004b7a2b18 --- /dev/null +++ b/python/GafferSceneUI/ImageScatterUI.py @@ -0,0 +1,158 @@ +########################################################################## +# +# 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 IECore + +import Gaffer +import GafferScene + +########################################################################## +# Metadata +########################################################################## + +Gaffer.Metadata.registerNode( + + GafferScene.ImageScatter, + + "description", + """ + Scatters points across an image, using pixel values to control the density + of the points. Arbitrary image channels may be converted to additional + primitive variables on the points, and point width may also be driven by an + image channel. + + > Note : Only the area of the `displayWindow` is considered. To + > include overscan pixels, use a Crop node to extend the display + > window. + """, + + plugs = { + + "sets" : [ + + "layout:divider", True, + + ], + + "image" : [ + + "description", + """ + The image used to drive the point scattering process. + """, + + "nodule:type", "GafferUI::StandardNodule", + + ], + + "view" : [ + + "description", + """ + The view within the image to be used by the scattering process. + """, + + "plugValueWidget:type", "GafferImageUI.ViewPlugValueWidget", + "layout:divider", True, + + ], + + "density" : [ + + "description", + """ + The overall density of the scattered points, defined in points + per pixel. + """ + + ], + + "densityChannel" : [ + + "description", + """ + The image channel used to modulate the density of the scattered points. + Black pixels will receive no points and white pixels will receive the + full amount as defined by the `density` plug. + """ + + ], + + "primitiveVariables" : [ + + "description", + """ + The image channels to be converted to primitive variables on + the points. The chosen channels are converted using the + following rules : + + - The main `RGB` channels are converted to a colour primitive variable called `Cs`. + - `.RGB` channels are converted to a colour primitive variable called ``. + - Other channels are converted to individual float primitive variables. + """, + + "plugValueWidget:type", "GafferImageUI.ChannelMaskPlugValueWidget", + + ], + + "width" : [ + + "description", + """ + The width of the points. If `widthChannel` is used as well, then this acts as + a multiplier on the channel values. + """ + + ], + + "widthChannel" : [ + + "description", + """ + The channel used to provide per-point width values for the points. + """, + + "plugValueWidget:type", "GafferImageUI.ChannelPlugValueWidget", + "channelPlugValueWidget:imagePlugName", "image", + "channelPlugValueWidget:extraChannels", IECore.StringVectorData( [ "" ] ), + "channelPlugValueWidget:extraChannelLabels", IECore.StringVectorData( [ "None" ] ), + "layout:divider", True, + + ], + + } + +) diff --git a/python/GafferSceneUI/__init__.py b/python/GafferSceneUI/__init__.py index 62e2f920e33..9f4122972d7 100644 --- a/python/GafferSceneUI/__init__.py +++ b/python/GafferSceneUI/__init__.py @@ -186,6 +186,7 @@ from . import FramingConstraintUI from . import MeshNormalsUI from . import LightToolUI +from . import ImageScatterUI # then all the PathPreviewWidgets. note that the order # of import controls the order of display. diff --git a/src/GafferScene/ImageScatter.cpp b/src/GafferScene/ImageScatter.cpp new file mode 100644 index 00000000000..ed54ee306c6 --- /dev/null +++ b/src/GafferScene/ImageScatter.cpp @@ -0,0 +1,371 @@ +////////////////////////////////////////////////////////////////////////// +// +// 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. +// +////////////////////////////////////////////////////////////////////////// + +#include "GafferScene/ImageScatter.h" + +#include "GafferImage/ImageAlgo.h" +#include "GafferImage/Sampler.h" + +#include "Gaffer/StringPlug.h" + +#include "IECoreScene/PointsPrimitive.h" + +#include "IECore/PointDistribution.h" + +#include "tbb/parallel_for.h" + +using namespace Gaffer; +using namespace GafferScene; +using namespace GafferImage; +using namespace IECore; +using namespace IECoreScene; +using namespace Imath; +using namespace std; + +////////////////////////////////////////////////////////////////////////// +// Internal utilities +////////////////////////////////////////////////////////////////////////// + +namespace +{ + +void sampleChannel( const ImagePlug *image, const Box2i &displayWindow, const string &channelName, const vector &positions, const IECore::Canceller *canceller, float *outData, int stride, float multiplier = 1.0f ) +{ + Sampler sampler( image, channelName, displayWindow, Sampler::Clamp ); + sampler.populate(); // Multithread the population of image tiles + + tbb::task_group_context taskGroupContext( tbb::task_group_context::isolated ); + tbb::parallel_for( tbb::blocked_range( 0, positions.size() ), + [&] ( const tbb::blocked_range &range ) { + IECore::Canceller::check( canceller ); + for( size_t i = range.begin(); i < range.end(); ++i ) + { + outData[i*stride] = sampler.sample( positions[i].x, positions[i].y ) * multiplier; + } + }, + taskGroupContext + ); +} + +} // namespace + +////////////////////////////////////////////////////////////////////////// +// ImageScatter +////////////////////////////////////////////////////////////////////////// + +GAFFER_NODE_DEFINE_TYPE( ImageScatter ); + +size_t ImageScatter::g_firstPlugIndex = 0; + +ImageScatter::ImageScatter( const std::string &name ) + : ObjectSource( name, "points" ) +{ + storeIndexOfNextChild( g_firstPlugIndex ); + addChild( new ImagePlug( "image" ) ); + addChild( new StringPlug( "view", Plug::In, "default" ) ); + addChild( new FloatPlug( "density", Plug::In, 0.5f, 0.0f ) ); + addChild( new StringPlug( "densityChannel", Plug::In, "R" ) ); + addChild( new StringPlug( "primitiveVariables" ) ); + addChild( new FloatPlug( "width", Plug::In, 1.0f ) ); + addChild( new StringPlug( "widthChannel" ) ); +} + +ImageScatter::~ImageScatter() +{ +} + +GafferImage::ImagePlug *ImageScatter::imagePlug() +{ + return getChild( g_firstPlugIndex ); +} + +const GafferImage::ImagePlug *ImageScatter::imagePlug() const +{ + return getChild( g_firstPlugIndex ); +} + +Gaffer::StringPlug *ImageScatter::viewPlug() +{ + return getChild( g_firstPlugIndex + 1 ); +} + +const Gaffer::StringPlug *ImageScatter::viewPlug() const +{ + return getChild( g_firstPlugIndex + 1 ); +} + +Gaffer::FloatPlug *ImageScatter::densityPlug() +{ + return getChild( g_firstPlugIndex + 2 ); +} + +const Gaffer::FloatPlug *ImageScatter::densityPlug() const +{ + return getChild( g_firstPlugIndex + 2 ); +} + +Gaffer::StringPlug *ImageScatter::densityChannelPlug() +{ + return getChild( g_firstPlugIndex + 3 ); +} + +const Gaffer::StringPlug *ImageScatter::densityChannelPlug() const +{ + return getChild( g_firstPlugIndex + 3 ); +} + +Gaffer::StringPlug *ImageScatter::primitiveVariablesPlug() +{ + return getChild( g_firstPlugIndex + 4 ); +} + +const Gaffer::StringPlug *ImageScatter::primitiveVariablesPlug() const +{ + return getChild( g_firstPlugIndex + 4 ); +} + +Gaffer::FloatPlug *ImageScatter::widthPlug() +{ + return getChild( g_firstPlugIndex + 5 ); +} + +const Gaffer::FloatPlug *ImageScatter::widthPlug() const +{ + return getChild( g_firstPlugIndex + 5 ); +} + +Gaffer::StringPlug *ImageScatter::widthChannelPlug() +{ + return getChild( g_firstPlugIndex + 6 ); +} + +const Gaffer::StringPlug *ImageScatter::widthChannelPlug() const +{ + return getChild( g_firstPlugIndex + 6 ); +} + +void ImageScatter::affects( const Plug *input, AffectedPlugsContainer &outputs ) const +{ + ObjectSource::affects( input, outputs ); + + if( + input == viewPlug() || + input == imagePlug()->viewNamesPlug() || + input == imagePlug()->channelNamesPlug() || + input == densityChannelPlug() || + input == widthChannelPlug() || + input == imagePlug()->formatPlug() || + input == imagePlug()->dataWindowPlug() || + input == imagePlug()->channelDataPlug() || + input == densityPlug() || + input == widthPlug() || + input == primitiveVariablesPlug() + ) + { + outputs.push_back( sourcePlug() ); + } +} + +void ImageScatter::hashSource( const Gaffer::Context *context, IECore::MurmurHash &h ) const +{ + ImagePlug::ViewScope viewScope( context ); + const std::string view = viewPlug()->getValue(); + viewScope.setViewNameChecked( &view, imagePlug()->viewNames().get() ); + + ConstStringVectorDataPtr channelNamesData = imagePlug()->channelNamesPlug()->getValue(); + const string densityChannel = densityChannelPlug()->getValue(); + if( !ImageAlgo::channelExists( channelNamesData->readable(), densityChannel ) ) + { + throw IECore::Exception( fmt::format( "Density channel \"{}\" does not exist", densityChannel ) ); + } + + const string widthChannel = widthChannelPlug()->getValue(); + if( widthChannel.size() && !ImageAlgo::channelExists( channelNamesData->readable(), widthChannel ) ) + { + throw IECore::Exception( fmt::format( "Width channel \"{}\" does not exist", widthChannel ) ); + } + + const Format format = imagePlug()->formatPlug()->getValue(); + const Box2i &displayWindow = format.getDisplayWindow(); + Sampler densitySampler( imagePlug(), densityChannel, displayWindow, Sampler::Clamp ); + + h.append( displayWindow ); + h.append( format.getPixelAspect() ); + densitySampler.hash( h ); + densityPlug()->hash( h ); + + widthPlug()->hash( h ); + h.append( widthChannel ); + + const std::string primitiveVariablesMatchPattern = primitiveVariablesPlug()->getValue(); + for( const auto &channelName : channelNamesData->readable() ) + { + if( channelName == widthChannel || StringAlgo::matchMultiple( channelName, primitiveVariablesMatchPattern ) ) + { + h.append( channelName ); + Sampler sampler( imagePlug(), channelName, displayWindow, Sampler::Clamp ); + sampler.hash( h ); + } + } +} + +IECore::ConstObjectPtr ImageScatter::computeSource( const Context *context ) const +{ + // Validate input image. + + ImagePlug::ViewScope viewScope( context ); + const std::string view = viewPlug()->getValue(); + viewScope.setViewNameChecked( &view, imagePlug()->viewNames().get() ); + + ConstStringVectorDataPtr channelNamesData = imagePlug()->channelNamesPlug()->getValue(); + const string densityChannel = densityChannelPlug()->getValue(); + if( !ImageAlgo::channelExists( channelNamesData->readable(), densityChannel ) ) + { + throw IECore::Exception( fmt::format( "Density channel \"{}\" does not exist", densityChannel ) ); + } + + const string widthChannel = widthChannelPlug()->getValue(); + if( widthChannel.size() && !ImageAlgo::channelExists( channelNamesData->readable(), widthChannel ) ) + { + throw IECore::Exception( fmt::format( "Density channel \"{}\" does not exist", widthChannel ) ); + } + + const Format format = imagePlug()->formatPlug()->getValue(); + const Box2i &displayWindow = format.getDisplayWindow(); + const float pixelAspect = format.getPixelAspect(); + const Box2f outputArea = Box2f( + V2f( displayWindow.min.x * pixelAspect, displayWindow.min.y ), + V2f( displayWindow.max.x * pixelAspect, displayWindow.max.y ) + ); + + // Generate positions using a PointDistribution reading from a Sampler for + // the density channel. + + Sampler densitySampler( imagePlug(), densityChannel, displayWindow, Sampler::Clamp ); + densitySampler.populate(); // Multithread the population of image tiles + + // Point distribution is designed for samping within a unit square, so we + // offset and scale to fit that to our input image. + const float scale = std::max( outputArea.size().x, outputArea.size().y ); + const V2i offset = displayWindow.min; + + auto densityFunction = [&] ( const V2f &p ) { + IECore::Canceller::check( context->canceller() ); + return densitySampler.sample( offset.x + p.x * scale / pixelAspect, offset.y + p.y * scale ); + }; + + V3fVectorDataPtr positionsData = new V3fVectorData; + positionsData->setInterpretation( IECore::GeometricData::Point ); + vector &positions = positionsData->writable(); + auto emitter = [&] ( const V2f &p ) { + positions.push_back( V3f( offset.x + p.x * scale, offset.y + p.y * scale, 0.0f ) ); + }; + + /// \todo It would be nice to multithread this, but it could also be pretty + /// handy that the order of the points we're outputting matches the + /// progressive order in which they are generated. + PointDistribution::defaultInstance()( + Box2f( V2f( 0 ), V2f( outputArea.size() ) / scale ), + // Scale density to be in points per pixel + densityPlug()->getValue() * scale * scale, + densityFunction, + emitter + ); + + // Make a PointsPrimitive from the positions + + PointsPrimitivePtr result = new PointsPrimitive( positionsData ); + + // Add on primitive variables. + + const float width = widthPlug()->getValue(); + if( widthChannel.empty() ) + { + result->variables["width"] = PrimitiveVariable( PrimitiveVariable::Interpolation::Constant, new FloatData( width ) ); + } + + const std::string primitiveVariablesMatchPattern = primitiveVariablesPlug()->getValue(); + for( const auto &channelName : channelNamesData->readable() ) + { + if( StringAlgo::matchMultiple( channelName, primitiveVariablesMatchPattern ) ) + { + const int colorIndex = ImageAlgo::colorIndex( channelName ); + if( colorIndex >= 0 && colorIndex <= 2 ) + { + // Map R, G and B to the components of colour primitive variables. + // This is the same behaviour as ImageToPoints. + string name = ImageAlgo::layerName( channelName ); + name = name == "" ? "Cs" : name; + Color3fVectorDataPtr colorData = result->variableData( name ); + if( !colorData ) + { + colorData = new Color3fVectorData; + colorData->writable().resize( positions.size() ); + result->variables[name] = PrimitiveVariable( PrimitiveVariable::Vertex, colorData ); + } + sampleChannel( imagePlug(), displayWindow, channelName, positions, context->canceller(), colorData->baseWritable() + colorIndex, 3 ); + } + else + { + // Map everything else to individual float primitive variables. + const string name = channelName == widthChannel ? "width" : channelName; + FloatVectorDataPtr floatData = new FloatVectorData; + floatData->writable().resize( positions.size() ); + result->variables[name] = PrimitiveVariable( PrimitiveVariable::Vertex, floatData ); + sampleChannel( imagePlug(), displayWindow, channelName, positions, context->canceller(), floatData->writable().data(), 1 ); + } + } + + if( channelName == widthChannel ) + { + FloatVectorDataPtr widthData = new FloatVectorData; + widthData->writable().resize( positions.size() ); + result->variables["width"] = PrimitiveVariable( PrimitiveVariable::Vertex, widthData ); + sampleChannel( imagePlug(), displayWindow, channelName, positions, context->canceller(), widthData->writable().data(), 1, width ); + } + } + + return result; +} + +Gaffer::ValuePlug::CachePolicy ImageScatter::computeCachePolicy( const Gaffer::ValuePlug *output ) const +{ + if( output == sourcePlug() ) + { + return ValuePlug::CachePolicy::TaskCollaboration; + } + return ObjectSource::computeCachePolicy( output ); +} diff --git a/src/GafferSceneModule/PrimitivesBinding.cpp b/src/GafferSceneModule/PrimitivesBinding.cpp index 6ce673256cd..41b61b2c923 100644 --- a/src/GafferSceneModule/PrimitivesBinding.cpp +++ b/src/GafferSceneModule/PrimitivesBinding.cpp @@ -46,6 +46,7 @@ #include "GafferScene/Cube.h" #include "GafferScene/ExternalProcedural.h" #include "GafferScene/Grid.h" +#include "GafferScene/ImageScatter.h" #include "GafferScene/ImageToPoints.h" #include "GafferScene/Light.h" #include "GafferScene/ObjectToScene.h" @@ -150,6 +151,7 @@ void GafferSceneModule::bindPrimitives() GafferBindings::DependencyNodeClass(); GafferBindings::DependencyNodeClass(); GafferBindings::DependencyNodeClass(); + GafferBindings::DependencyNodeClass(); GafferBindings::DependencyNodeClass(); { diff --git a/startup/gui/menus.py b/startup/gui/menus.py index fda32d78966..4943bac94f5 100644 --- a/startup/gui/menus.py +++ b/startup/gui/menus.py @@ -247,6 +247,7 @@ def __lightCreator( nodeName, shaderName, shape ) : nodeMenu.append( "/Scene/File/Writer", GafferScene.SceneWriter, searchText = "SceneWriter" ) nodeMenu.append( "/Scene/Source/Object To Scene", GafferScene.ObjectToScene, searchText = "ObjectToScene" ) nodeMenu.append( "/Scene/Source/Image To Points", GafferScene.ImageToPoints, searchText = "ImageToPoints" ) +nodeMenu.append( "/Scene/Source/Image Scatter", GafferScene.ImageScatter, searchText = "ImageScatter" ) nodeMenu.append( "/Scene/Source/Camera", GafferScene.Camera ) nodeMenu.append( "/Scene/Source/Coordinate System", GafferScene.CoordinateSystem, searchText = "CoordinateSystem" ) nodeMenu.append( "/Scene/Source/Clipping Plane", GafferScene.ClippingPlane, searchText = "ClippingPlane" ) From df69c4cd3f24730a93e7cdd46fbcaba54fc1a7fb Mon Sep 17 00:00:00 2001 From: John Haddon Date: Fri, 20 Oct 2023 10:42:20 +0100 Subject: [PATCH 3/3] ImageSampler : Fix primitive variables from non-square pixels --- python/GafferSceneTest/ImageScatterTest.py | 57 ++++++++++++---------- src/GafferScene/ImageScatter.cpp | 10 ++-- 2 files changed, 37 insertions(+), 30 deletions(-) diff --git a/python/GafferSceneTest/ImageScatterTest.py b/python/GafferSceneTest/ImageScatterTest.py index 3ac759ed404..03a2e8a1053 100644 --- a/python/GafferSceneTest/ImageScatterTest.py +++ b/python/GafferSceneTest/ImageScatterTest.py @@ -90,9 +90,9 @@ def testMissingChannels( self ) : def testPrimitiveVariables( self ) : ramp = GafferImage.Ramp() - ramp["format"].setValue( GafferImage.Format( 2, 3 ) ) - ramp["startPosition"].setValue( imath.V2f( 0, 0.5 ) ) - ramp["endPosition"].setValue( imath.V2f( 0, 2.5 ) ) + 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 ) @@ -107,29 +107,36 @@ def testPrimitiveVariables( self ) : self.assertEqual( set( points.keys() ), { "P", "width" } ) scatter["primitiveVariables"].setValue( "[RGBA]" ) - 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 - y = points["P"].data[i].y - y = (y - 0.5) / 2.0 - y = max( 0, min( y, 1 ) ) - # Should match what we sampled for `Cs` - self.assertAlmostEqual( c[0], y, delta = 0.000001 ) - self.assertAlmostEqual( c[1], y, delta = 0.000001 ) - self.assertAlmostEqual( c[2], y, delta = 0.000001 ) - # And width should be double that - self.assertAlmostEqual( points["width"].data[i], y * 2, delta = 0.000001 ) - self.assertEqual( - points["A"].data, - IECore.FloatVectorData( [ 1.0 ] * len( points["P"].data ) ) - ) + 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 ) : diff --git a/src/GafferScene/ImageScatter.cpp b/src/GafferScene/ImageScatter.cpp index ed54ee306c6..770c59d58c0 100644 --- a/src/GafferScene/ImageScatter.cpp +++ b/src/GafferScene/ImageScatter.cpp @@ -62,7 +62,7 @@ using namespace std; namespace { -void sampleChannel( const ImagePlug *image, const Box2i &displayWindow, const string &channelName, const vector &positions, const IECore::Canceller *canceller, float *outData, int stride, float multiplier = 1.0f ) +void sampleChannel( const ImagePlug *image, const Box2i &displayWindow, const string &channelName, const vector &positions, float pixelAspect, const IECore::Canceller *canceller, float *outData, int stride, float multiplier = 1.0f ) { Sampler sampler( image, channelName, displayWindow, Sampler::Clamp ); sampler.populate(); // Multithread the population of image tiles @@ -73,7 +73,7 @@ void sampleChannel( const ImagePlug *image, const Box2i &displayWindow, const st IECore::Canceller::check( canceller ); for( size_t i = range.begin(); i < range.end(); ++i ) { - outData[i*stride] = sampler.sample( positions[i].x, positions[i].y ) * multiplier; + outData[i*stride] = sampler.sample( positions[i].x / pixelAspect, positions[i].y ) * multiplier; } }, taskGroupContext @@ -336,7 +336,7 @@ IECore::ConstObjectPtr ImageScatter::computeSource( const Context *context ) con colorData->writable().resize( positions.size() ); result->variables[name] = PrimitiveVariable( PrimitiveVariable::Vertex, colorData ); } - sampleChannel( imagePlug(), displayWindow, channelName, positions, context->canceller(), colorData->baseWritable() + colorIndex, 3 ); + sampleChannel( imagePlug(), displayWindow, channelName, positions, pixelAspect, context->canceller(), colorData->baseWritable() + colorIndex, 3 ); } else { @@ -345,7 +345,7 @@ IECore::ConstObjectPtr ImageScatter::computeSource( const Context *context ) con FloatVectorDataPtr floatData = new FloatVectorData; floatData->writable().resize( positions.size() ); result->variables[name] = PrimitiveVariable( PrimitiveVariable::Vertex, floatData ); - sampleChannel( imagePlug(), displayWindow, channelName, positions, context->canceller(), floatData->writable().data(), 1 ); + sampleChannel( imagePlug(), displayWindow, channelName, positions, pixelAspect, context->canceller(), floatData->writable().data(), 1 ); } } @@ -354,7 +354,7 @@ IECore::ConstObjectPtr ImageScatter::computeSource( const Context *context ) con FloatVectorDataPtr widthData = new FloatVectorData; widthData->writable().resize( positions.size() ); result->variables["width"] = PrimitiveVariable( PrimitiveVariable::Vertex, widthData ); - sampleChannel( imagePlug(), displayWindow, channelName, positions, context->canceller(), widthData->writable().data(), 1, width ); + sampleChannel( imagePlug(), displayWindow, channelName, positions, pixelAspect, context->canceller(), widthData->writable().data(), 1, width ); } }