[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
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.
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!