Image Sampler Node Update

Hi All,

I am testing out another workflow using some hand drawings, I have a thought of using the image sampler but my skills in Visual Studio are not great.

The old ImageSamplerNode visual studio code has some updates needed. I was wondering if anyone knew where I would start looking for information on what to change. 

There are "type or namespace" errors in lines

using Bentley.GenerativeComponents.Nodes;

[Replicatable, ParentNodeScope] int ArrPtsX,

I am making a guess but would this be a UtilityNode as it doesn't produce geometry, it sort of functions like the calculator node in the sample addin?

 

Any pointers would be appreciated.

Thanks

Wayne

The script in full is:

 

// Source adapted from Ralf Lindemann's imageUVSampler
// which had been based code by Marc Hoppermann (www.21cd.org)


using System;
using System.Drawing;
using System.Collections.Generic;
//using System.Windows.Forms;
//using Bentley.Geometry;
using Bentley.GenerativeComponents;
using Bentley.GenerativeComponents.Features;
using Bentley.GenerativeComponents.GCScript.NameScopes;
using Bentley.GenerativeComponents.GeneralPurpose;
using Bentley.GenerativeComponents.GCScript;
using Bentley.GenerativeComponents.MicroStation;
using Bentley.GenerativeComponents.View;
using Bentley.Interop.MicroStationDGN;
using Bentley.GenerativeComponents.GCScript.GCTypes;
using Bentley.GenerativeComponents.GCScript.ReflectedNativeTypeSupport;
using Bentley.GenerativeComponents.Nodes;

namespace ImageSampler
{
[GCNamespace("User")] // This custom attribute specifies that, when this node type is loaded into GC, it will be put into
// the GC namespace "User". (So, within GC, this node type's full name will be "User.ImageSamplerNode".)

[NodeTypePaletteCategory("Utility")] // The NodeTypePaletteCategory attribute lets us specify where this
// Calculator node type will appear within GC's Node Types dialog.
// So, it will appear within a group named "Sample Add-In".

[NodeTypeIcon("Resources/ImageSamplerNode.png")] // The NodeTypeIcon attribute lets us specify the graphical image (icon)
// that will appear on the Calculator node type's button within GC's Node
// Types dialog.


public class ImageSampler: Feature
{
/// <summary>ImageSampler reading pixels</summary>
[Technique]
public NodeUpdateResult ReadPixels
(
NodeUpdateContext updateContext,
[Replicatable, ParentNodeScope] int ArrPtsX,
[Replicatable] int ArrPtsY,
[Replicatable] int GridH,
[Replicatable] int GridV,
string ImagePath,
[Out] ref int Width,
[Out] ref int Height,
[Out] ref int R,
[Out] ref int G,
[Out] ref int B,
[Out] ref double Grayscale
)
{
double x = ArrPtsX;
double y = ArrPtsY;
if(false == System.IO.File.Exists(ImagePath))
{
Width = 0;
Height = 0;
R = 0;
G = 0;
B = 0;
Grayscale = 0.0;
return new NodeUpdateResult.IncompleteInputs("ImagePath");
}

Bitmap myBitmap = new Bitmap(ImagePath);
Width = myBitmap.Width;
Height = myBitmap.Height;

// this logic needs review
double stepX = Width / GridH;
double stepY = Height / GridV;
double pixelPosX = stepX * x;
double pixelPosY = stepY * y;

// get pixel information
Color pixelValue = myBitmap.GetPixel(Convert.ToInt32(pixelPosX), Convert.ToInt32(pixelPosY));

R = pixelValue.R;
G = pixelValue.G;
B = pixelValue.B;

// calculate gray scale
Grayscale = (0.3 * R + 0.59 * G + 0.11 * B) / 255.0;

return NodeUpdateResult.Success;
}

}
}

Parents
  • Anik is right, probably your best source of information is the latest version of GC's sample solution.

    Looking at your source code, I see the following two issues, which would prevent it from compiling under the latest released version:

    The namespace Bentley.GenerativeComponents.Nodes is now named Bentley.GenerativeComponents.UtilityNodes.

    The attribute ParentNodeScope is now named DgnModelProvider. But, you should just delete that attribute, anyway. Your input parameter, ArrPtsX, is just a basic 'int' type, so it would never carry any information about a parent node scope or a DGN model.

    HTH

    Answer Verified By: Wayne Dickerson 

  • Thanks Jeff,

    I suspected the namespace of UtilityNode. 

    Thanks for the tips on the ParentNodeScope.

    I had been looking at the Calculator script and trying to adapt that script as I thought it would be a similar node not generating any geometry in the file.

    What I was a bit confused about was in the Calculator script it lists

    public class Calculator: UtilityNode

    which makes sense to me as it is creating a utilitynode.

    In the imagesampler code it uses:

    public class ImageSampler: Feature

    Which is more inline with the SimpleLine example. 

    I assume it is related to this comment.

    // One of the fundamental differences between the Feature-based node architecture and the Node-based node architecture
    // is that, in the former, reflection is used to extract the technique names, the documentation, and the names and types
    // of the inputs and outputs, directly from the compiled C# class. GC provides a number of custom attributes to provide
    // more information to the that reflection process.

    I need to do some more C# work.

    Is there any Bentley Documentation on things like the attributes or namespaces used in GC?

    But for now the Image Sampler is up and running, now to make some adjustments to suit the workflow I have in mind.

    Thanks

    Wayne

  • Hi Wayne,

    If I understand correctly, you want to know the count of the replicatable inputs ArrPtsX & ArrPtsY? If so you should be able to access this by simply writing ArrPtsX.Length, since it is an array. If you want to know the specific index of the current iteration (from GC replication), you can access this through this.ReplicationIndex.

    Edit - just had a deeper look and can see your predicament. Since it is not defined as an "array" due to using the GC parameter flag [Replicatable] instead, it will not expose the method ".Length". You could try the following:

    - this.MaxReplicationCount

    - this.ListCount(null);

    Not sure if that works but worth a try!

    Cheers,

    Ed

  • Thanks Ed,

    Yeah I am looking for the count of the inputs. I was just writing as you sent it. Yeah I was assuming as GC is generating the replication it is only sending a single parameter each time it runs the code (visual studio). 

    I will give your suggestion a try. 

    Thanks

    Wayne

  • I added the first line

    gave it an output to see what it was doing

    returned this which is interesting.

    Now need to work out what to do with it in VS :)

  • sorry I just realised that you are after the count for each individual input list which would likely be different to the total replication count. In that case perhaps try ArrPtsX.ListCount(null)  -  you may need to first check that it is a list using ArrPtsX.IsList()

    Also I assume from the result you got it is returning that 4 x 4 list, since you have 4 replicatable parameters with a count of 4 objects in each. The 0 would likely be representative of the number of objects within each of those input objects (but since the rank = 2 the list goes no further, so the count = 0 for each list item). All speculation of course so I am sure Jeff can let us know about that hunch!

  • Maybe Jeff can let us know how this type of node works. 

    My basic thoughts are as the ArrPtsX is an int it is really only accepting a single value each time? 

    Maybe GC does the replication in the program as you can see the stacked node. If it was passing in array it would be a single node?

    Anyway. 

    I did some more trials with what knowledge I have at the moment. 

    I create a list and tried to pass into it the information from GC.

    The results I think show that each time it is only passing in 1 item as the count is 1? But looking at it now, it is a count so it probably is only a single value anyway (MaxReplicationCount) so I am just adding one item to the list hence the results.

    The array shown here might just be the node replication.

    Anyway more reading tonight might help Slight smile

    Thanks

    Wayne

Reply
  • Maybe Jeff can let us know how this type of node works. 

    My basic thoughts are as the ArrPtsX is an int it is really only accepting a single value each time? 

    Maybe GC does the replication in the program as you can see the stacked node. If it was passing in array it would be a single node?

    Anyway. 

    I did some more trials with what knowledge I have at the moment. 

    I create a list and tried to pass into it the information from GC.

    The results I think show that each time it is only passing in 1 item as the count is 1? But looking at it now, it is a count so it probably is only a single value anyway (MaxReplicationCount) so I am just adding one item to the list hence the results.

    The array shown here might just be the node replication.

    Anyway more reading tonight might help Slight smile

    Thanks

    Wayne

Children
  • Yeah that was my understanding as well... When you give it the replicatable parameter flag this tells GC to run your code as a recursive function, iterating through each individual item in the array. So when you debug, at replication index 0 you will only see the first value in your ArrPtsX, not the full array; hence you are not getting the result you were after with the code above since GC has only exposed one value from the array to that specific replication index.

    In theory we should be able to get around this by using methods that refer directly to the feature (node) as a whole, as we can then access the input parameters in their unaltered state. This is why this.maxreplicationcount can see a count for each index in the array, for each replicatable parameter. The only issue is we don't really know which input parameter is which, because they are just given an index instead of the parameter name.

    BTW did you have any luck trying ArrPtsX.ListCount(null) method? 

  • As has been pointed out, when you use replication, your technique method sees only one item at a time; it doesn't see the overall input values that were provided by the user. GC actually creates a new, hidden node instance for each combination of individual input items.

    If I was doing this, I would abandon replication altogether, and handle the multiplicity of inputs myself, within the one technique method.

    I would start by replacing all the replicated inputs of type 'int' with non-replicated inputs of type 'int[]' (array of ints). For example:

    • [Replicatable] int ArrPtsX

    would become:

    • int[] ArrPtsX

    Similarly, the output parameters such as

    • [Out] ref int Width

    would be redefined as:

    • [Out] ref int[] Width

    That way, the technique method would have full sight of all the input values, and full control over what is done with those values. Furthermore, the WayneImageSampler node will be much more efficient, since GC won't need to create all those hidden node instances.

    Of course, this complicates the technique method, since it will now need to iterate through all the input combinations, itself. But, it's actually not that difficult.

    Following this approach, I would take your existing functionality and move it into a separate method (just an ordinary class method, not a GC technique method); let's call that method 'OneShot'. Then, the technique method, itself, would mainly comprise nested 'foreach' loops that iterate through the input arrays, and call OneShot at the innermost level of those nested loops.

    To handle each of the output parameters, I would start (at the top of the technique method) by creating a list of ints into which we will accumulate the individual output values that are returned by OneShot. For example:

    • List<int> widthList = new List<int>();

    And then, at the deepest nested level of the 'foreach' loops:

    • OneShot(..., , out int w , ...);
    • widthList.Add(w);

    Then, at the end of the technique method, before it returns NodeUpdateResult.Success, we can say

    • Width = widthList.ToArray();

    HTH. As an additional bonus, you'll learn more about C#. Slight smile

    Jeff 

    Answer Verified By: Wayne Dickerson 

  • Thanks Jeff,

    They were the tips needed I think.

    Not sure it it is the best way to do it but I did learn a bit about C# Methods and jagged arrays etc.

    Below is a copy of where I got up to. It is all working well so far. I changed up the way it works as I thought it was probably easier to ask for the horizontal and vertical divisions rather than a series etc So now you only need to place an int for how many divisions you would like in either direction.

    As always I have a few additional questions. 

    I setup the bools so I could select if I wanted to find the centre point or the average. 

    I have found that they even if you don't want them checked you need to set them on then off to make the node complete.

    Is there a way around this by setting them on or off as default?

    The only other item that would be nice I think is a browse option for the image path. Is this something that is easy to implement?

    Here is an example of what the output can do Slight smile

    heights and colours all coming from the input image. (you can guess what image it comes from!)

    here is the script

    using System;
    using System.Drawing;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using Bentley.GenerativeComponents;
    using Bentley.GenerativeComponents.GCScript;
    using Bentley.GenerativeComponents.GCScript.GCTypes;
    using Bentley.GenerativeComponents.GCScript.NameScopes;
    using Bentley.GenerativeComponents.GCScript.ReflectedNativeTypeSupport;
    using Bentley.GenerativeComponents.GeneralPurpose;
    using Bentley.GenerativeComponents.UtilityNodes;
    using Bentley.GenerativeComponents.View;
    using Bentley.GenerativeComponents.Features;
    using Bentley.GenerativeComponents.ScriptEditor;
    
    namespace SampleAddIn
    {
        [GCNamespace("User")]                                   // This custom attribute specifies that, when this node type is loaded into GC, it will be put into
                                                                // the GC namespace "User". (So, within GC, this node type's full name will be "User.ImageSamplerNode".)
    
        [NodeTypePaletteCategory("Sample Add-In")]              // The NodeTypePaletteCategory attribute lets us specify where this
                                                                // Calculator node type will appear within GC's Node Types dialog.
                                                                // So, it will appear within a group named "Sample Add-In".
    
        [NodeTypeIcon("Resources/ImageSamplerNode.png")]        // The NodeTypeIcon attribute lets us specify the graphical image (icon)
                                                                // that will appear on the Calculator node type's button within GC's Node
                                                                // Types dialog.
    
    
        public class WayneImageSampler : Feature
        {
            /// <summary>ImageSampler reading pixels</summary>
            [Technique]
            public NodeUpdateResult ReadPixels
            (
                NodeUpdateContext updateContext,
                // ask the user just for a gird in horizontal and vertical
                //no need to get them to create the series
                int GridH,
                int GridV,
                // Maybe add some checkboxes to decide if we wanted to calculate the centre pixel or an average
                bool CentrePt,
                bool AverageResults,
                //FolderPathBrowser ImagePath = new FolderPathBrowser.get
                //string  ImagePath = FolderPathBrowser,
                string ImagePath,
    
                // Setup the output that will appear on the node. 
                [Out] ref int[][] R,
                [Out] ref int[][] G,
                [Out] ref int[][] B,
                [Out] ref double[][] Grayscale,
                [Out] ref int Width,
                [Out] ref int Height
    
    
            )
            {
              
                if (false == System.IO.File.Exists(ImagePath))
                {
                    return new NodeUpdateResult.IncompleteInputs("ImagePath");
                }
    
                List<int> RColourList = new List<int>();
                List<int> GColourList = new List<int>();
                List<int> BColourList = new List<int>();
                List<double> GSColourList = new List<double>();
    
                //Defined some 2d jagged arrays
                int[][] RColourArray = new int[GridV][];
                int[][] GColourArray = new int[GridV][];
                int[][] BColourArray = new int[GridV][];
                double[][] GSRColourArray = new double[GridV][];
                int ImgWidth = 0;
                int ImgHeight = 0;
    
                for (int i = 0; i < GridV; i++)
                {
                    RColourArray[i] = new int[GridH];
                    GColourArray[i] = new int[GridH];
                    BColourArray[i] = new int[GridH];
                    GSRColourArray[i] = new double[GridH];
    
    
                    for (int j = 0; j < GridH; j++)
                    {                    
                        int y = i;
                        int x = j;
                        int Rcolour = 0;
                        int Gcolour = 0;
                        int Bcolour = 0;
                        double GScolour = 0;
    
    
                        SamplePixel(x, y, ImagePath, GridH, GridV, CentrePt, AverageResults, out Rcolour, out Gcolour, out Bcolour, out GScolour, out ImgWidth, out ImgHeight);
                        RColourArray[i][j] = Rcolour;
                        GColourArray[i][j] = Gcolour;
                        BColourArray[i][j] = Bcolour;
                        GSRColourArray[i][j] = GScolour;
    
                    }
    
    
                }
                    R = RColourArray;
                    G = GColourArray;
                    B = BColourArray;
                    Grayscale = GSRColourArray;
                    Width = ImgWidth;
                    Height = ImgHeight;
    
                    return NodeUpdateResult.Success;
                }
    
    
    
                void SamplePixel(int HNumber, int VNumber, string ImgPath, int TotalGridH, int TotalGridV, bool ctr, bool avg, out int Rcolour, out int Gcolour, out int Bcolour, out double GScolour, out int ImgWidth, out int ImgHeight)
                {
    
                    int Width;
                    int Height;
    
    
    
                    Bitmap myBitmap = new Bitmap(ImgPath);
                    Width = myBitmap.Width;
                    Height = myBitmap.Height;
                
                    // output so we could use it to setout some geometry?
                    ImgWidth = Width;
                    ImgHeight = Height;
    
                // this logic needs review
                // Assume that it would be best to have the GridH and GridV match the count of the 
                // series(*,*,*) that is used for ArrPtsX and ArrPtsY. Then you would get a step across the image divided by the series count.
                // If this count coul be passed into this process they wouldn't need to be input by the user?
                double stepX = Width / TotalGridH;
                    double stepY = Height / TotalGridV;
                    double pixelPosX = stepX * HNumber;
                    double pixelPosY = stepY * VNumber;
                    // This pixel position is at the start of the sample square ie stepY * 0? Does that work pixel 0
                    // This could be adjusted to find the centre of the sample square.
                    if (ctr)
                    {
                        pixelPosX = pixelPosX + stepX / 2;
                        pixelPosY = pixelPosY + stepY / 2;
                    }
    
    
                    // Could you get an average of the sample square of pixels
                    // It could be done with a loop of the stepX and stepY
                    // Then add all teh pixelValues together and divide by stepX*stepY
                    // This could take a while for a large image. 4000*3000 would be 12Million calculations
                    // Maybe a percentage say 10% of the pixels would be ok.
                    // or maybe better is like photoshop get a 4x4 sample area of pixels. We could even start with a 5 samples,
                    // one in the centre and then offset say 2 pixels each way
    
                    if (avg)
                    {
                        double pixelPosx1 = pixelPosX;
    
                        double pixelPosx2 = pixelPosX + 2;
                        if (pixelPosx2 > Width)
                        {
                            pixelPosx2 = Width;
                        }
                        double pixelPosx3 = pixelPosX - 2;
                        if (pixelPosx3 < 0)
                        {
                            pixelPosx3 = 0;
                        }
                        double pixelPosy1 = pixelPosY;
    
                        double pixelPosy2 = pixelPosY + 2;
                        if (pixelPosy2 > Height)
                        {
                            pixelPosy2 = Height;
                        }
                        double pixelPosy3 = pixelPosY - 2;
                        if (pixelPosy3 < 0)
                        {
                            pixelPosy3 = 0;
                        }
    
    
                        Color pixelValue1 = myBitmap.GetPixel(Convert.ToInt32(pixelPosx1), Convert.ToInt32(pixelPosy1));
                        Color pixelValue2 = myBitmap.GetPixel(Convert.ToInt32(pixelPosx2), Convert.ToInt32(pixelPosy1));
                        Color pixelValue3 = myBitmap.GetPixel(Convert.ToInt32(pixelPosx3), Convert.ToInt32(pixelPosy1));
                        Color pixelValue4 = myBitmap.GetPixel(Convert.ToInt32(pixelPosx1), Convert.ToInt32(pixelPosy2));
                        Color pixelValue5 = myBitmap.GetPixel(Convert.ToInt32(pixelPosx1), Convert.ToInt32(pixelPosy3));
    
                        Rcolour = (pixelValue1.R + pixelValue2.R + pixelValue3.R + pixelValue4.R + pixelValue5.R) / 5;
                        Gcolour = (pixelValue1.G + pixelValue2.G + pixelValue3.G + pixelValue4.G + pixelValue5.G) / 5;
                        Bcolour = (pixelValue1.B + pixelValue2.B + pixelValue3.B + pixelValue4.B + pixelValue5.B) / 5;
    
    
                    }
                    else
                    {
                        // get pixel information
                        Color pixelValue = myBitmap.GetPixel(Convert.ToInt32(pixelPosX), Convert.ToInt32(pixelPosY));
    
                        Rcolour = pixelValue.R;
                        Gcolour = pixelValue.G;
                        Bcolour = pixelValue.B;
                    }
                    // calculate gray scale
                    GScolour = (0.3 * Rcolour + 0.59 * Gcolour + 0.11 * Bcolour) / 255.0;
    
    
    
    
    
    
    
    
    
                }
    
            }
        
    }
    
        
    

    Thanks

    Wayne

  • Looks great, Wayne! Wonderful!

    You can provide an explicit default value for any of your inputs by using the 'InitialValue' attribute, like this (for example):

    [InitialValue(true)] bool CentrePt,
    [InitialValue(false)] bool AverageResults,

    Whenever you use the 'InitialValue' attribute, take care that the value you specify is of the same type as the input you're applying it to.

    Regarding your other question: Unfortunately, it's not feasible -- yet -- to provide a browser option for the image path. GC can do that for its own nodes' inputs, but that mechanism isn't exposed externally. I've added that to my to-do list for the next update.

    Answer Verified By: Wayne Dickerson