[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
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).
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.?
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?
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.
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.