Notifications
Clear all

[Closed] Bitmap Processing Efficiencies

Alright Pete, good news! I got the C# GetPixels function working on full scanlines now, so the image can be processed completely in C# and it will return full floating point values with no clamping. Time for a 4k image is about 0.6 seconds.

   The method I found to use GetPixels is to create a stand-in struct that contains 4 floats as its members (that replaces the IBMM_Color_fl interface), and then allocate a pointer with Marshal.AllocHGlobal. We then get an IBMM_Color_fl object by using the Marshal function of an instance of the __Global.IGlobalBMM_Color_fl class, and pass that object to the GetPixels function, making sure to allocate its size as sizeof(float)*4*widthOfImage. GetPixels will then assign values from the scanline, and the resulting values can be obtained using pointer arithmetic on the pointer that was allocated. 
   
   It's not as fast as a C++ alternative, but having a C# version obtainable with maxscript is very convenient.
   
    Here's the final script:

        if classof (dotnet.GetType "processBitmap") != dotNetObject then
        (
        	classStr = "		
        	
        	using System;	
        	using Autodesk.Max;
        	using Autodesk.Max.Wrappers;
        	using System.Runtime.InteropServices;
        	  
        	struct pixelColor //stand in struct to use w/marshal
        	{
        		public float R;
        		public float G;
        		public float B;
        		public float A;
        	}
        	
        	class processBitmap
        	{			
        		public unsafe static float[] processbitmap (System.UIntPtr handle)
        		{			
        			float min = 1000000;
        			float max = -1000000;
        			
        			BitmapTex tex = (BitmapTex)GlobalInterface.Instance.Animatable.GetAnimByHandle(handle);
        			IBitmap bmp = tex.GetBitmap(0);
        						
        			__Global.IGlobalBMM_Color_fl ctr = new __Global.__GlobalBMM_Color_fl();						
        						
        			int structSize = sizeof(float)*4; //BMM_Color_fl has 4 floats RGBA as main parameters
  
        			 int wid = bmp.Width;
  			int hei = bmp.Height;
   
        			IntPtr pointer = Marshal.AllocHGlobal(structSize*wid);			
        			IBMM_Color_fl line = ctr.Marshal(pointer);
        		   
        			for (int i = 0; i < hei; i++)
        			{	
        				bmp.GetPixels(0, i, wid, line);
        				
        				for (int j = 0; j < wid; j++)
        				{
        					IntPtr ptr = new IntPtr(pointer.ToInt64() + j * structSize);	
        					pixelColor* px = (pixelColor*)ptr;
        										
        					float val = ((*px).R*.3f + (*px).G*.59f + (*px).B*.11f);	
        					min = Math.Min(min, val);
        					max = Math.Max(max, val);
        					
        				}
        			}
        			
        			Marshal.FreeHGlobal(pointer);
        			
        			return new float[]{min, max};
        		}
        	
        	}
        	"
        	compilerParams = dotnetobject "System.CodeDom.Compiler.CompilerParameters"
        	dotnet.setlifetimecontrol compilerParams #dotnet
        		
        	compilerParams.CompilerOptions = "/unsafe"
        	compilerParams.ReferencedAssemblies.Add("System.dll");
        	compilerParams.ReferencedAssemblies.Add("System.Drawing.dll");			
        	compilerParams.ReferencedAssemblies.Add((getdir #maxroot) + @"Autodesk.Max.dll");	
        	compilerParams.ReferencedAssemblies.Add((getdir #maxroot) + @"\bin\assemblies\Autodesk.Max.Wrappers.dll");
        	compilerParams.ReferencedAssemblies.Add((getdir #maxroot)+ @"ManagedServices.dll");
        	compilerParams.ReferencedAssemblies.Add((getdir #maxroot)+ @"MaxCustomControls.dll");
        	compilerParams.GenerateInMemory = on
        	csharpProvider = dotnetobject "Microsoft.CSharp.CSharpCodeProvider"
        		
        	compilerResults = csharpProvider.CompileAssemblyFromSource compilerParams #(classStr)
        	dotnet.setlifetimecontrol compilerResults #dotnet
        
        	if (compilerResults.Errors.Count > 0 ) then
        	(		
        		local errs = stringstream ""
        		for i = 0 to (compilerResults.Errors.Count-1) do
        		(
        			local err = compilerResults.Errors.Item[i]
        			format "Error:% Line:% Column:% %
" err.ErrorNumber err.Line err.Column err.ErrorText to:errs
        		)
        		format "%
" errs
        		undefined
        	)
        )
        
        fn processBitmap map =
        (		
        	st = timestamp()--get start time in milliseconds	
        	
        	tmp = bitmaptexture()
        	tmp.bitmap = map
        		
        	minmax = (dotnetclass "processBitmap").processbitmap (dotnet.valuetodotnetobject (gethandlebyanim tmp) (dotnetclass "System.UIntptr"))
        	
        	format "Per Pixel Line Min/Max Processing took % seconds : % %x%
" ((timestamp()-st) / 1000.0) map.filename map.width map.height
        	print minmax
        )
        
        
    Here's how to call it:

        processBitmap <bitmaptexture> --the bitmap image must be assigned to a bitmaptexture, because we get the bitmap from C# through the handle of its parent texture
        
        
    And here are the results:

        Per Pixel Line Min/Max Processing took 0.06 seconds : C:	mp\minmax_sm.exr 1024x1024
        
        Per Pixel Line Min/Max Processing took 0.187 seconds : C:	mp\minmax_med.exr 2048x2048
        
        Per Pixel Line Min/Max Processing took 0.66 seconds : C:	mp\minimax_lg.exr 4096x4096
        
        
1 Reply
(@lonerobot)
Joined: 10 months ago

Posts: 0

Well it sure is my lucky day. That’s an outstanding piece of work Tyson. Thanks a million :buttrock: Matches the output of the curvetool luma analysis in Nuke exactly.

And thanks to everyone else for the suggestions and optimisations, it is much appreciated.

Minor optimization.
If you cache the bitmap width in this line:

for (int j = 0; j < [b]bmp.Width[/b]; j++)

the code should be 3X faster.

Thanks for that, I’ve edited the code above with the change. 4k image in 0.25 seconds now. Approx 180x faster than maxscript alternative…not bad

“__Global.IGlobalBMM_Color_fl ctr = new __Global.__GlobalBMM_Color_fl();”

Hi Tyson. What is __Global in your code? I can’t find any ‘IGlobalBMM_Color_fl’ anywhere, nor ‘__GlobalBMM_Color_fl’.

Just for trying to compile it directly from Visual Studio.

Are you including “Autodesk.Max.Wrappers.DLL” in your project? It’s in [maxroot]/bin/assemblies.

Definitions for ‘__Global’ and ‘__GlobalBMM_Color_fl’ are in the wrappers DLL, and the definition for ‘IGlobalBMM_Color_fl’ is in the ‘Autodesk.Max.DLL’ which I’m assuming you’re already including in your project.

The only thing I’m not sure about is how many version of Max support these classes…I’m using 3dsmax2016, but it’s possible older version don’t have them.

1 Reply
(@aaandres)
Joined: 10 months ago

Posts: 0

Yes. All it’s included. Must be my 3DSMAX Design 2014 version.

You could use something like dotPeek to check your DLLs and see if they contain the classes in question. That’s how I originally found them.

Is it possible to use the same method instead of DoesFileExist because it is very slow especially for network locations, or this is completely other story? Currently I”m checking the file status for an entire list, and I do it in bgworker.

Hey Tyson and Pete,

I came across this post a little late it seems, but this was really cool to look at. I needed a tool like this one too. I went and wrapped it all up into a MaxScript struct that we can use a a module in our pipeline at work. I figured I’d just share that here in case anyone else needed something like this.

I also updated it so that it just takes a path to an image file rather than needing a Bitmap. This way you can pass it either a <BitmapTexture>.filename or a <VRayHDRi>.HDRiMapName, or simply just a full path to a file.

Code:


	/*
	__HELP__

	Constructor: GetMinMaxPixels
	Instantiated Global: GetMinMaxPixels

	Methods:
		FromFile <path to file>


	__END__
	*/



	struct GetMinMaxPixels
	(
	public

		cClass,
		
		fn GetCSharpStr = (),
		fn InitDotNetClass = (),
		
		fn FromFile mapFile =
		(
			if not ( DoesFileExist mapFile ) then
			(
				local str = StringStream ""
				format "Could not find image file at:
%
" mapFile to:str
				messageBox ( str as string ) title:"File Error:"
				return undefined
			)
			
			if ( this.cClass != undefined ) then
			(
				local st = timestamp() --get start time in milliseconds	
				
				local tmp = bitmaptexture filename:mapFile
					
				local minmax = this.cClass.getMinMaxPixels ( dotnet.ValueToDotNetObject ( Gethandlebyanim tmp ) ( dotnetclass "System.UIntptr" ) )
				
				format "Per Pixel Line Min/Max Processing took % seconds : % | %x% | min:%, max:%
" \
				((timestamp()-st) / 1000.0) ( FilenameFromPath tmp.filename ) tmp.bitmap.width tmp.bitmap.height minmax[1] minmax[2]
				
				minmax
			)
		),

	private
		
		fn GetCSharpStr =
		(
			local classStr = "
	using System;
	using Autodesk.Max;
	using Autodesk.Max.Wrappers;
	using System.Runtime.InteropServices;

	struct pixelColor //stand in struct to use w/marshal
	{
		public float R;
		public float G;
		public float B;
		public float A;
	}

	class GetMinMaxPixels
	{
		public unsafe static float[] getMinMaxPixels (System.UIntPtr handle)
		{
			float min = 1000000;
			float max = -1000000;
			
			BitmapTex tex = (BitmapTex)GlobalInterface.Instance.Animatable.GetAnimByHandle(handle);
			IBitmap bmp = tex.GetBitmap(0);
			
			__Global.IGlobalBMM_Color_fl ctr = new __Global.__GlobalBMM_Color_fl();
			
			int structSize = sizeof(float)*4; //BMM_Color_fl has 4 floats RGBA as main parameters

			int wid = bmp.Width;
			int hei = bmp.Height;
			
			IntPtr pointer = Marshal.AllocHGlobal(structSize*wid);
			IBMM_Color_fl line = ctr.Marshal(pointer);
		   
			for (int i = 0; i < hei; i++)
			{
				bmp.GetPixels(0, i, wid, line);
				
				for (int j = 0; j < wid; j++)
				{
					IntPtr ptr = new IntPtr(pointer.ToInt64() + j * structSize);
					pixelColor* px = (pixelColor*)ptr;
					
					float val = ((*px).R*.3f + (*px).G*.59f + (*px).B*.11f);
					min = Math.Min(min, val);
					max = Math.Max(max, val);
				}
			}
			
			Marshal.FreeHGlobal(pointer);
			
			return new float[]{min, max};
		}
	}
			"
			
			classStr
		),
		
		fn InitDotNetClass =
		(
			local compilerParams = dotnetobject "System.CodeDom.Compiler.CompilerParameters"
			dotnet.setlifetimecontrol compilerParams #dotnet
				
			compilerParams.CompilerOptions = "/unsafe"
			compilerParams.ReferencedAssemblies.Add ("System.dll")
			compilerParams.ReferencedAssemblies.Add ("System.Drawing.dll")
			compilerParams.ReferencedAssemblies.Add ((getdir #maxroot) + @"Autodesk.Max.dll")
			compilerParams.ReferencedAssemblies.Add ((getdir #maxroot) + @"\bin\assemblies\Autodesk.Max.Wrappers.dll")
			compilerParams.ReferencedAssemblies.Add ((getdir #maxroot)+ @"ManagedServices.dll")
			compilerParams.ReferencedAssemblies.Add ((getdir #maxroot)+ @"MaxCustomControls.dll")
			compilerParams.GenerateInMemory = on
				
			local csharpProvider = dotnetobject "Microsoft.CSharp.CSharpCodeProvider"
				
			local compilerResults = csharpProvider.CompileAssemblyFromSource compilerParams #( ( this.GetCSharpStr() ) )
			dotnet.setlifetimecontrol compilerResults #dotnet
			
			if ( compilerResults.Errors.Count > 0 ) then
			(		
				local errs = stringstream ""
				for i = 0 to (compilerResults.Errors.Count-1) do
				(
					local err = compilerResults.Errors.Item[i]
					format "Error:% Line:% Column:% %
" err.ErrorNumber err.Line err.Column err.ErrorText to:errs
				)
				--format "%
" errs
				throw errs
				return undefined
			)
			
			format "** C# Class Initialized **
"
			
			( dotnetclass "GetMinMaxPixels" )
		),

		fn _init =
		(
			if ( ( MaxVersion() )[1] >= 18000 ) then -- Check if the 3dsmax version is at least 3dsmax 2016, C# SDK is different in older versions
			(
				this.cClass = this.InitDotNetClass()
			)
			else
			(
				messageBox "This module is not compatible with 3dsmax versions prior to 2016" title:"Max Version:"
			)
		),

		__init__ = _init()
	)

	GetMinMaxPixels = GetMinMaxPixels()
	-- minmax = GetMinMaxPixels.FromFile <Path to image file>

Thanks again! This works amazing!

Page 2 / 2