Notifications
Clear all

[Closed] Bitmap Processing Efficiencies

Hi,

Thought this was a question for all you code optimising geeks out there. I’m processing a 32 bit linear exr Displacement map to get the max/min values of image. I know max is pretty slow for this sort of thing, and i’ve tried most methods I know to increase the speed of the process. I realise append is slow, but I’m seeing interesting results when the array gets over a certain size. For example, on a 1k image, pre-declaring the array is faster, but on a 4k image it takes much longer than append. Here are the three methods I’ve tried. Is there any trick or method I’ve missed here?

	-- APPEND
	fn getBitmapMaxMinValues map res:1.0 =
	(	
		if res != 1.0 then
		(
		nBmp = bitmap (map.width*res) (map.height*res) hdr:true	
		copy map nBmp
		map = nBmp	
		format "Map Rescaled to % x % pixels
" map.width map.height
		)	
		
		st = timestamp()--get start time in milliseconds
		lumaArray = #()
		-- append is the slow part of this operation
		setwaitcursor()
		for h = 1 to map.height do
		(
			pixel_line = getpixels map [0,(h-1)] map.width
			for pix in pixel_line do			
				append lumaArray (0.3*pix.r + 0.59*pix.g + 0.11*pix.b)				
		)
		
		format "Append Processing took % seconds : % %x%
" ((timestamp()-st) / 1000.0) map.filename map.width map.height
		[((amin lumaArray)/255.), ((amax lumaArray)/255.)]
	)	

	-- COMPARE 
	fn getBitmapMaxMinValues_v2 map res:1.0 =
	(	
		if res != 1.0 then
		(
		nBmp = bitmap (map.width*res) (map.height*res) hdr:true	
		copy map nBmp
		map = nBmp	
		format "Map Rescaled to % x % pixels
" map.width map.height
		)	
		
		st = timestamp()--get start time in milliseconds
		lumaArray = [0,0]
		-- append is the slow part of this operation
		for h = 1 to map.height do
		(
			pixel_line = getpixels map [0,(h-1)] map.width
			row = for pix in pixel_line collect									
				(0.3*pix.r + 0.59*pix.g + 0.11*pix.b)
			
			if (mx = amax row) > (lumaArray.y) then (lumaArray.y = mx)
			if (mn = amin row) < (lumaArray.x) then (lumaArray.x = mn)			
		)
		
		format "Per Pixel Line Min/Max Processing took % seconds : % %x%
" ((timestamp()-st) / 1000.0) map.filename map.width map.height
		[ lumaArray.x/255., lumaArray.y/255.]		
	)
	-- PRE DECLARE ARRAY
	fn getBitmapMaxMinValues_v3 map res:1.0 =
	(	
		if res != 1.0 then
		(
		nBmp = bitmap (map.width*res) (map.height*res) hdr:true	
		copy map nBmp
		map = nBmp	
		format "Map Rescaled to % x % pixels
" map.width map.height
		)	
		
		st = timestamp()--get start time in milliseconds
		lumaArray = #()
		lumaArray[map.width * map.height] = 0
		
		-- append is the slow part of this operation
		setwaitcursor()		
		
		for h = 1 to map.height do
		(
			pixel_line = getpixels map [0,(h-1)] map.width
			for pix = 1 to pixel_line.count do	
			(								
				lumaArray[pix+(h-1)* map.width] = (0.3*pixel_line[pix].r + 0.59*pixel_line[pix].g + 0.11*pixel_line[pix].b)
			)			
		)
		
		format "Pre-Declared Processing took % seconds : % %x%
" ((timestamp()-st) / 1000.0) map.filename map.width map.height
		[((amin lumaArray)/255.), ((amax lumaArray)/255.)]		

	)

Results…


isPresent true : C:\Users\Slipknot\Desktop\Penguin_Body_v05__DISP.exr Vray HDRI true
	
Map Rescaled to 1024 x 1024 pixels array size:1048576

Append Processing took 5.955 seconds :  1024x1024
Per Pixel Line Min/Max Processing took 6.005 seconds :  1024x1024
Pre-Declared Processing took 3.801 seconds :  1024x1024

Map Rescaled to 2048 x 2048 pixels array size:4194304

Append Processing took 9.932 seconds :  2048x2048
Per Pixel Line Min/Max Processing took 9.096 seconds :  2048x2048
Pre-Declared Processing took 18.196 seconds :  2048x2048

4k Map array size:16777216

Append Processing took 45.509 seconds : 4096x4096
Per Pixel Line Min/Max Processing took 45.681 seconds : 4096x4096
Pre-Declared Processing took 66.944 seconds : 4096x4096
20 Replies

Hmm, I’m unable to replicate your <=1024px pre-declare performance. The pre-declare function is pretty consistently about 1.5-2x slower than the other two in my tests, regardless of resolution (I tested everything from 128sq to 4096sq pixels).

 lo1

For me v2 is faster than v1 for 2048×2048, with v3 being the slowest.

Other than rewriting this in C++ (seriously, this would take like 3ms to execute), or making it multithreaded in maxscript, I think you’ve done all you can.

Even C# could do the trick if you’re ok with the color values of your bitmap being rounded to integers and clamped between 0-255.

This runs in 0.7 seconds on a 4k image…although I’m guessing you’re relying on 32bit EXRs for a reason and need the floating point values. Shame that SetClipboardBitmap clamps everything.


   global pb_classIter	
   
   if (pb_classIter == undefined) then pb_classIter = 100	
   pb_class = "processBitmap_" + pb_classIter as string
   pb_classIter += 1
   
   if classof (dotnet.GetType pb_class) != dotNetObject then
   (
   	classStr = "		
   	
   	using System;
   	using System.Drawing;
   	using System.Drawing.Imaging;
   	
   	class " + pb_class + "
   	{
   		public unsafe static float[] processbitmap (Bitmap map)
   		{
   			float min = 1000000;
   			float max = -100000;
   	
   	
   			BitmapData bData = map.LockBits(new Rectangle(0, 0, map.Width, map.Height), ImageLockMode.ReadWrite, map.PixelFormat);
   
   			byte bitsPerPixel = (byte)Image.GetPixelFormatSize(bData.PixelFormat);
   
   			byte* scan0 = (byte*)bData.Scan0.ToPointer();
   
   			for (int i = 0; i < bData.Height; ++i)
   			{
   				for (int j = 0; j < bData.Width; ++j)
   				{
   					byte* data = scan0 + i * bData.Stride + j * bitsPerPixel / 8;
   					float val = (0.3f*data[0] + 0.59f*data[1] + 0.11f*data[2]);	
   					min = Math.Min(min, val);
   					max = Math.Max(max, val);
   				}
   			}
   
   			map.UnlockBits(bData);
   	
   	
   			return new float[]{min/255.0f, max/255.0f};
   		}
   	
   	}
   	"
   	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)+ @"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 getBitmapMaxMinValues_v4 map res:1.0 =
   (	
   	if res != 1.0 then
   	(
   	nBmp = bitmap (map.width*res) (map.height*res) hdr:true	
   	copy map nBmp
   	map = nBmp	
   	format "Map Rescaled to % x % pixels
" map.width map.height
   	)	
   	
   	
   	
   	st = timestamp()--get start time in milliseconds
   	
   	setClipboardBitmap map -- Copy the bitmap to the clipboard to pass it to dotNet	
   	clipboardObj = dotNetClass "System.Windows.Forms.Clipboard"  -- Create a dotNet clipboard object.	
   	imgObj = clipboardObj.GetImage() -- Get the image from the clipboard into a dotNet image object.
   	
   	minmax = ((dotnetclass pb_class).processbitmap imgObj)
   	
   		
   	format "Per Pixel Line Min/Max Processing took % seconds : % %x%
" ((timestamp()-st) / 1000.0) map.filename map.width map.height
   	minmax
   )
   

 getBitmapMaxMinValues_v4 b res:4
 
 Map Rescaled to 4096 x 4096 pixels
 Per Pixel Line Min/Max Processing took 0.668 seconds :  4096x4096
 #(0.0, 0.87902)
 

 

“Shame that SetClipboardBitmap clamps everything.”
I’ve never worked with bitmaps. Isn’t there any max object to hold the bitmap and then pass it to C# by handleByAnim instead of copying it to the clipboard.?

1 Reply
(@ivanisavich)
Joined: 10 months ago

Posts: 0

Good idea, I’ve managed to pass a bitmaptexture to .net and extract the bitmap from it…the only issue is that dotnet’s GetPixels function crashes max when retrieving more than 1 pixel at a time. Even when retrieving 1 pixel at a time the function runs faster than the maxscript equivalent, but it would probably be much faster still if I could figure out how to get it to read scanlines.

Here’s reading 1 pixel at a time:


  BitmapTex tex = (BitmapTex)GlobalInterface.Instance.Animatable.GetAnimByHandle(handle); //handle = (dotnet.valuetodotnetobject (gethandlebyanim [the bitmaptexture with the bitmap assigned]) (dotnetclass "System.UIntptr")
  
  IBitmap bmp = tex.GetBitmap(0);
  			
   __Global.IGlobalBMM_Color_fl ctr = new __Global.__GlobalBMM_Color_fl();					
 IBMM_Color_fl line = ctr.Create(0,0,0,0);
  						 
  			
  for (int i = 0; i < bmp.Height; i++)
  {
  	  for (int j = 0; j < bmp.Width; j++)
  	  {										
  			  bmp.GetPixels(j, i, 1, line); //reading more than 1 pixel at a time crashes max...even when j=0??
  					
  			   float val = (line.R*.3f + line.G*.59f + line.B*.11f);   
  		}
  }
  
  

Here’s the code for the GetPixels function, inside the Autodesk.Max DLL:


  public virtual unsafe int GetPixels(int x, int y, int pixels, IBMM_Color_fl ptr)
  	{
  	  global::BMM_Color_fl* bmmColorFlPtr1 = ptr == null ? (global::BMM_Color_fl*) 0L : BMM_Color_fl.__ConvertToUnmanaged(ptr);
  	  ulong num1 = (ulong) *(long*) ((IntPtr) this.unmanaged() + 56L);
  	  if ((long) num1 == 0L)
  		return 0;
  	  long num2 = (long) num1;
  	  int num3 = x;
  	  int num4 = y;
  	  int num5 = pixels;
  	  global::BMM_Color_fl* bmmColorFlPtr2 = bmmColorFlPtr1;
  	  // ISSUE: cast to a function pointer type
  	  // ISSUE: function pointer call
  	  return __calli((__FnPtr<int (IntPtr, int, int, int, global::BMM_Color_fl*)>) *(long*) (*(long*) num1 + 240L))((global::BMM_Color_fl*) num2, num3, num4, num5, (IntPtr) bmmColorFlPtr2);
  	}
  
  

Does anyone have any ideas why it would crash when ‘pixels’ > 1?

 lo1

You’re not allocating any memory for the pixels, you’re only creating one. I’m not familiar with the c# SDK, but ideally you’d create an array the size of a row and pass a pointer to that.

Thanks everyone for your input. Even in the current state, it’s quicker than the round trip to nuke (and using a nuke license for that matter) to do a curvetool luma analysis. And yes Tyson, full float is really important as it’s being used as a displacement map. Even loading it via the max bitmap loader introduces noise that affects the render (the VrayHDRI loader doesn’t have this problem) so it’s important to be as accurate as possible.

Thanks also for your SDK suggestions, I regret to say that I know many programming languages, but C++ is just gobbledegook to me.

 lo1

Another suggestion: read every 2nd, 3rd or 4th row. I would bet the error compared to reading every row will be negligible in almost all cases.

Any idea how to do that? I’m not super familiar with pointers in C#. I tried something like:


__Global.IGlobalBMM_Color_fl ctr = new __Global.__GlobalBMM_Color_fl();	
IBMM_Color_fl[] pix = new IBMM_Color_fl[bmp.Width];
GCHandle handle1 = GCHandle.Alloc(pix);		
IBMM_Color_fl ptr = ctr.Marshal((IntPtr)handle1);
bmp.GetPixels(0,0,bmp.Width,ptr)
handle1.Free();

…but it doesn’t work.

This is all guesswork on my part since these C# max functions aren’t documented…but the GetPixels function only takes a IBMM_Color_fl object as its input, and the Marshal function of the wrapper class seems to convert a pointer to an IBMM_Color_fl object…so I’m assuming the steps to get a row of pixels is create an array of IBMM_Color_fl, then get a pointer to that array and use Marshal on the pointer to get a IBMM_Color_fl object and pass that to the GetPixels function…?

I know you said you’re not familiar with the C# SDK, but maybe you could offer advice about how to allocate an array and get a pointer to it.

 lo1

No such luck, they have really made this anything but easy.

Page 1 / 2