SDK typo/discrepancy ? bitmap.Init() regarding bitdepth
-
I was struggling because of this ...
In the Documentation: BaseBitmap.Init it states:
depth (int) –The requested bit depth (24 default).The possible values are {1,4,8,16,24,32}.
Which led me to the asumption its per channel .... but its not!
I was creating a code reading a 32 bit per channel image per pixel from the renderer.Hence the BaseBitmap.GetPixelCnt needs the corect inc (int) – The byte increment per pixel in the buffer. i was calculating on the wrong source bitrate ...
My code works now after I set
bmp.Init(width, height, 96)
instead of the before mentioned 32!This might be a small thing and people more fluent in bits and bytes will notice ... but i was keeping myself strict to the sdk till I found old code from niklas calculating the inc from an loaded 32 bit image (which divided the image bit by 3 and checked against 32 -> which led me to 96)
anyway feel free corect me, but for me this seems like a typo in the sdk ...
kind regards mogh
here is some sample code for folks pasing by:
import c4d import struct from c4d import bitmaps c4d.CallCommand(13957) # Get the active document doc = c4d.documents.GetActiveDocument() bd_now = doc.GetRenderBaseDraw() bd_now.GetEditorCamera() # Get the active render data rd = doc.GetActiveRenderData().GetData() # Set the coordinate of the pixel you want to read both = 32 width = both height = both rd[c4d.RDATA_XRES] = both rd[c4d.RDATA_YRES] = both bmp = c4d.bitmaps.BaseBitmap() bmp.Init(width, height, 96) result = c4d.documents.RenderDocument(doc, rd, bmp, c4d.RENDERFLAGS_EXTERNAL) if result==c4d.RENDERRESULT_OK: bitmaps.ShowBitmap(bmp) print(bmp.GetBt()) # Set up the parameters for GetPixelCnt() cnt = 4 inc = bmp.GetBt() // 8 # membuffer = c4d.storage.ByteSeq(None, cnt * inc) # python 2.7 membuffer = bytearray(cnt * inc) dstmode = c4d.COLORMODE_RGBf print(dstmode) flags = c4d.PIXELCNT_0 """ # Read the pixel value pixel_value = bmp.GetPixelCnt(width-1, height-1, cnt, membuffer, inc, dstmode, flags) # Convert the byte values to 32-bit floating point values r, = struct.unpack('f', membuffer[0:4]) g, = struct.unpack('f', membuffer[4:8]) b, = struct.unpack('f', membuffer[8:12]) a, = struct.unpack('f', membuffer[12:16]) # Print the pixel value #print((r, g, b, a)) """ # https://developers.maxon.net/forum/topic/11154/setpixel-for-32-bit-float-images/2 # Loop through all the pixels in the bitmap and read their values for y in range(height): for x in range(width): # Read the pixel value bmp.GetPixelCnt(x, y, cnt, membuffer, inc, dstmode, flags) # Convert the byte values to 32-bit floating point values r, = struct.unpack('f', membuffer[0:4]) g, = struct.unpack('f', membuffer[4:8]) b, = struct.unpack('f', membuffer[8:12]) a, = struct.unpack('f', membuffer[12:16]) # Print the pixel value if r or g or b or a != 0: print("Pixel at ({}, {}): (R:{}, G:{}, B:{}, A:{})".format(x, y, r, g, b, a))
-
Hello @mogh,
Thank you for reaching out to us.
depth (int) –The requested bit depth (24 default).The possible values are {1,4,8,16,24,32}.
Which led me to the asumption its per channel .... but its not!Yes, that is how the
BaseBitmap
type historically and its now underlying type ImageInterface (not available in Python) have handled the concept of bit depth; as something which is the sum of the sizes of the channels of a pixel. In the Image API (not available in Python) this is much more fleshed out with the concept of pixel formats (not available in Python). I recently documented this in a bit more detail in the Color Management Manual:// One of the core functionalities of a pixel format is to describe the memory layout of that // format. This will become relevant when pixels must be converted between formats and/or color // spaces. // The number of channels/components of this pixel format, three in this case. const maxon::Int channelCount = pixRgbF32.GetChannelCount(); // The size of each channel in bits, the block [32, 32, 32] in this case, there exists also an // alias for the Block<Bits> return type, maxon::ChannelOffsets. const maxon::Block<const maxon::BITS> channelSizes = pixRgbF32.GetChannelOffsets(); // The total size of a pixel in bits, i.e., the sum of GetChannelOffsets(). const maxon::BITS pixelSize = pixRgbF32.GetBitsPerPixel(); // A pixel format also provides access to image channels, a more precise description of each // channel. In most cases accessing these from a pixel format is not required. for (const maxon::ImageChannel& channel : pixRgbF32.GetChannels()) { // The size of the pixel channel, i.e., component, in bits. const maxon::BITS bits = channel.GetChannelBits(); // The channel type which provides among other things access to the associated color space and // the default value for this channel for a pixel. const maxon::ImageChannelType channelType = channel.GetChannelType(); // The data type of the channel, e.g., Float32. const maxon::DataType dataType = channel.GetDataType(); ApplicationOutput("\t\tRGB::F32 channel '@' - Bits: @, ChannelType: @, DataType: @", pixRgbF32, pixRgbF32, pixRgbF32, pixRgbF32); }
I could update
BaseBitmap.Init
fordepth
pointing out that this value is meant as the sum the sizes of all channels, but that might look a bit odd since this is the common way color depths are expressed. There are of course varying forms of how such a bit depth per pixel is then interpreted as a channel layout, hence the need for the concept of pixel formats.So, long story short: What would you consider here to be a typo or missing information? That we do not mention the channel layout for each bit-depth, e.g, 8bit : (3, 3, 2)? Would that not be a bit fringe? Or do you simply want us to state that the value is meant as the sum of all channels?
The code you posted here still contains semantic errors; I am not sure if this was intended. bmp.Init(width, height, 96)
is incorrect code. The maximum bit depth a bitmap can have is 32, as cited by yourself above.Cheers,
Ferdinand -
@mogh said in SDK typo/discrepancy ? bitmap.Init() regarding bitdepth:
Which led me to the asumption its per channel .... but its not!
It was late - no excuse ... I meant "which led me to the asumption its combined"
I am sorry.@ferdinand said in SDK typo/discrepancy ? bitmap.Init() regarding bitdepth:
Would that not be a bit fringe? Or do you simply want us to state that the value is meant as the sum of all channels?
I want the min max number to be statet what
BaseBitmap.Init()
acecpts / or returns sane values ... ?!?
I interpreted the SDk that 32 is the max ...The code you posted here still contains semantic errors; I am not sure if this was intended. bmp.Init(width, height, 96) is incorrect code. The maximum bit depth a bitmap can have is 32, as cited by yourself above.
Now you confuse me - only 96 gives me correct values when I am reading the pixel values in 32 bit.
bmp.Init(width, height, 32) #converted gives c4d.Vector(1.0, 1.0, 1.0) bmp.Init(width, height, 96) #converted gives c4d.Vector(0.022, 0.012, 0.0) for example
So I am clearly running in circles hence the sdk states something different than example code on github and here in the forum.
Please forgive me, I might just run down the wrong path again ... I am trying to read Pixel colors from a
c4d.documents.RenderDocument()
passing an 32 bit image.kind regards
mogh -
Hey @mogh,
Now you confuse me - only 96 gives me correct values when I am reading the pixel values in 32 bit.
Yes, the documentation and I were wrong here, the type has changed. I am not fully sure though if there are any bugs in
BaseBitmap
in the wake of this. When the bitmap is initialized from a rendering, your code does work for the r/g/b components, although I do not understand fully why. When the image is 32 bits per channel, you should be unpacking a double,d
, and not a single,s
. What is definitely not correct is your alpha value, you leak there into the next pixel, because your buffer is actually four times the size of a pixel.The actual problems began when I loaded 32-bit images from disk and read their values; they are not correct. I am not quite sure yet why this is happening. It is not only that
GetPixelCnt
access fails but also GetPixelDirect, both return incorrect values for a 32bit PSD file with embedded alpha values. Both the RGB and the alpha values are incorrect.I will need a bit more time to figure out what is going on here.
Cheers,
Ferdinand -
Hi Ferdinand,
thank you for your thoroughness,
I dabbled a little bit with the suggestions you gave. But i have to admit I am a little bit out of my comfort zone with all that bytes anyway here is a more corect but still strange output giving code snippet ...green (0.0, 0.9, 0.1) pixel -> red readout => (R:0.00273437425494, G:0.0, B:0.0)
This completely wrong readout might just be me unfamiliar with bits and bytes ...Also
c4d.COLORMODE_RGBf
return 36 which is kinda strange, hence ist stated as 32bit 3 channels. But perhaps this is just an internal ID ...FYI: At the moment I do not have access to GetPixelDirect Thats why I use
struct.unpack()
Cheers,
moghimport c4d import struct import sys from c4d import bitmaps PYTHONVERSION = float(str(sys.version_info[0]) + "." + str(sys.version_info[1])) c4d.CallCommand(13957) # clear console # Get the active document doc = c4d.documents.GetActiveDocument() bd_now = doc.GetRenderBaseDraw() bd_now.GetEditorCamera() # Get the active render data rd = doc.GetActiveRenderData().GetData() both = 32 width = both height = both rd[c4d.RDATA_XRES] = both rd[c4d.RDATA_YRES] = both bmp = c4d.bitmaps.BaseBitmap() bmp.Init(width, height, 96) result = c4d.documents.RenderDocument(doc, rd, bmp, c4d.RENDERFLAGS_EXTERNAL) if result==c4d.RENDERRESULT_OK: bitmaps.ShowBitmap(bmp) # Set up the parameters for GetPixelCnt() cnt = 1 # = how many pixels ? inc = bmp.GetBt() // 8 # 96/8 = 12 = how many bytes ? dstmode = c4d.COLORMODE_RGBf flags = c4d.PIXELCNT_0 float_mem = 2 # double the buffer ??? if PYTHONVERSION < 3.5: membuffer = c4d.storage.ByteSeq(None, cnt * inc * float_mem) else: membuffer = bytearray(cnt * inc * float_mem) print(bmp.GetBt()) print(inc) print(dstmode) #https://developers.maxon.net/forum/topic/11154/setpixel-for-32-bit-float-images/2 # Loop through all the pixels in the bitmap and read their values for y in range(height): for x in range(width): # Read the pixel value bmp.GetPixelCnt(x, y, cnt, membuffer, inc, dstmode, flags) # Convert the byte values to 32-bit floating point values #r, = struct.unpack('f', membuffer[0:4]) #g, = struct.unpack('f', membuffer[4:8]) #b, = struct.unpack('f', membuffer[8:12]) #a, = struct.unpack('f', membuffer[12:16]) r, = struct.unpack('d', membuffer[0:8]) # f float single, d float double g, = struct.unpack('d', membuffer[8:16]) b, = struct.unpack('d', membuffer[16:24]) # Print the pixel value if not black if r or g or b != 0: print("Pixel at ({}, {}): (R:{}, G:{}, B:{})".format(x, y, r, g, b))
-
Hey @mogh,
But i have to admit I am a little bit out of my comfort zone with all that bytes
Not just you,
BaseBitmap
is really weird in how it views its data. This is likely caused byBaseBitmap
now only being a classic API adapter for an underlyingmaxon::ImageRef
. In C++, I would always recommend retrieving the underlying data with BaseBitmap::GetImageRef and operate on them, as this will make one's life much easier. In Python this is however not possible because the Image API has not been wrapped there.I dabbled a little bit with the suggestions you gave.
Sorry for sending you off in the wrong direction. What I meant here was: "You should be here reading floating point data with double precision, you are reading with single precision, it works, and double precision does not, and I do not yet understand why." I did of course try myself before and it did not yield what you would expect it to yield - but I was wrong there, it must be/is singles.
After some poking around, I figured out the details, especially the 'special' way
BaseBitmap
handles pixel formats and alpha channels. I will not go into details here; it is all explained in the code listing posted below. Only so much - what I dubbed as high-level access (BaseBitmap.GetPixelDirect
access demonstrated inGetBitmapDataHighLevel
in the example) is usually preferable over what I dubbed low-level access (BaseBitmap.GetPixelCnt
access demonstrated inGetBitmapDataLowLevel
in the example) as it will lead to much saner and more performant code.Also c4d.COLORMODE_RGBf return 36 which is kinda strange, hence ist stated as 32bit 3 channels. But perhaps this is just an internal ID ...
COLORMODE_RGBf
is a symbol for a color mode, not a bytes or bits value. It is just a coincidence that it is close to the number 32. The relevant symbol for what you want isc4d.COLORBYTES_RGBf
. The byte depth symbols are currently undocumented in Python but you can use them. SeeCOLOR_BYTES_TO_MODES
in my script below for details.Conclusion
- I will update
BaseBitmap.Init
so that it becomes clear thatdepth
can be1, 4, 8, 16, 24, 32, 48, or 96
- I will make clear that
BaseBitmap.GetBt
andBaseBitmap.GetBpz
cannot be trusted when initalizing pixel buffers forBaseBitmap.GetPixelCnt
for bitmaps which have an implicit alpha channel. - Despite this, it seems to be impossible to implicitly read alpha channels in one go, one must read the alpha channel manually.
- I will add the example to make it a bit more clear how to use
GetPixelCnt
with maxon/Image API pixel format symbols and as a warning that there is only little to gain here.
Cheers,
FerdinandPS: I am aware of the small discrepancies of reading alpha values with the script below. I have to talk with the relevant developers there, one might have to get more precise with the bit depth of the alpha channel which might not always be the same as for the color channels, and the discrepancy then being a rounding error caused by the up scaling of data.
File: test_getpixelcnt.zip - Contains the Python script, a test scene, and test bitmaps. Note that to run the script successfully, you must open the file instead of just pasting its content into the Script Manager because it searches for the bitmap files in relation to itself.
Result:
-------------------------------------------------------------------------------- rendering - bmp.GetSize() = (128, 128), bmp.GetBt() = 96, bmp.GetBpz() = 1536. Executing GetBitmapDataHighLevel() took 0.0092 seconds. Executing GetBitmapDataLowLevel() took 0.0095 seconds. (0, 0) high: Vector4d(0, 0, 0, 1) low : Vector4d(0, 0, 0, 1) (0, 1) high: Vector4d(0, 0, 0, 1) low : Vector4d(0, 0, 0, 1) (0, 2) high: Vector4d(0, 0, 0, 1) low : Vector4d(0, 0, 0, 1) (0, 3) high: Vector4d(0.006, 0.006, 0.008, 1) low : Vector4d(0.006, 0.006, 0.008, 1) (0, 4) high: Vector4d(0.064, 0.067, 0.088, 1) low : Vector4d(0.064, 0.067, 0.088, 1) (0, 5) high: Vector4d(0.161, 0.168, 0.221, 1) low : Vector4d(0.161, 0.168, 0.221, 1) (0, 6) high: Vector4d(0.248, 0.26, 0.341, 1) low : Vector4d(0.248, 0.26, 0.341, 1) (0, 7) high: Vector4d(0.258, 0.271, 0.355, 1) low : Vector4d(0.258, 0.271, 0.355, 1) (0, 8) high: Vector4d(0.259, 0.271, 0.355, 1) low : Vector4d(0.259, 0.271, 0.355, 1) (0, 9) high: Vector4d(0.262, 0.274, 0.358, 1) low : Vector4d(0.262, 0.274, 0.358, 1) -------------------------------------------------------------------------------- 32_0128.psd - bmp.GetSize() = (128, 4), bmp.GetBt() = 96, bmp.GetBpz() = 1536. Executing GetBitmapDataHighLevel() took 0.0003 seconds. Executing GetBitmapDataLowLevel() took 0.0005 seconds. (0, 0) high: Vector4d(0.969, 0.969, 0.969, 0.027) low : Vector4d(0.969, 0.969, 0.969, 0.031) (0, 1) high: Vector4d(0.97, 0.97, 0.97, 0.027) low : Vector4d(0.97, 0.97, 0.97, 0.03) (0, 2) high: Vector4d(0.97, 0.97, 0.97, 0.027) low : Vector4d(0.97, 0.97, 0.97, 0.03) (0, 3) high: Vector4d(0.97, 0.97, 0.97, 0.027) low : Vector4d(0.97, 0.97, 0.97, 0.03) (1, 0) high: Vector4d(0.893, 0.893, 0.893, 0.106) low : Vector4d(0.893, 0.893, 0.893, 0.107) (1, 1) high: Vector4d(0.891, 0.891, 0.891, 0.106) low : Vector4d(0.891, 0.891, 0.891, 0.109) (1, 2) high: Vector4d(0.891, 0.891, 0.891, 0.106) low : Vector4d(0.891, 0.891, 0.891, 0.109) (1, 3) high: Vector4d(0.892, 0.892, 0.892, 0.106) low : Vector4d(0.892, 0.892, 0.892, 0.108) (2, 0) high: Vector4d(0.814, 0.814, 0.814, 0.184) low : Vector4d(0.814, 0.814, 0.814, 0.186) (2, 1) high: Vector4d(0.814, 0.814, 0.814, 0.184) low : Vector4d(0.814, 0.814, 0.814, 0.186) -------------------------------------------------------------------------------- 32_1024.psd - bmp.GetSize() = (1024, 32), bmp.GetBt() = 96, bmp.GetBpz() = 12288. Executing GetBitmapDataHighLevel() took 0.0222 seconds. Executing GetBitmapDataLowLevel() took 0.0402 seconds. (0, 0) high: Vector4d(1, 1, 1, 0) low : Vector4d(1, 1, 1, 0) (0, 1) high: Vector4d(1, 1, 1, 0) low : Vector4d(1, 1, 1, 0) (0, 2) high: Vector4d(1, 1, 1, 0) low : Vector4d(1, 1, 1, 0) (0, 3) high: Vector4d(1, 1, 1, 0) low : Vector4d(1, 1, 1, 0) (0, 4) high: Vector4d(1, 1, 1, 0) low : Vector4d(1, 1, 1, 0) (0, 5) high: Vector4d(1, 1, 1, 0) low : Vector4d(1, 1, 1, 0) (0, 6) high: Vector4d(1, 1, 1, 0) low : Vector4d(1, 1, 1, 0) (0, 7) high: Vector4d(1, 1, 1, 0) low : Vector4d(1, 1, 1, 0) (0, 8) high: Vector4d(1, 1, 1, 0) low : Vector4d(1, 1, 1, 0) (0, 9) high: Vector4d(1, 1, 1, 0) low : Vector4d(1, 1, 1, 0) -------------------------------------------------------------------------------- test.psd - bmp.GetSize() = (128, 128), bmp.GetBt() = 96, bmp.GetBpz() = 1536. Executing GetBitmapDataHighLevel() took 0.0112 seconds. Executing GetBitmapDataLowLevel() took 0.0152 seconds. (0, 0) high: Vector4d(0, 0, 0, 0) low : Vector4d(0, 0, 0, 0) (0, 1) high: Vector4d(0, 0, 0, 0) low : Vector4d(0, 0, 0, 0) (0, 2) high: Vector4d(0, 0, 0, 0) low : Vector4d(0, 0, 0, 0) (0, 3) high: Vector4d(0.006, 0.006, 0.008, 0.02) low : Vector4d(0.006, 0.006, 0.008, 0.022) (0, 4) high: Vector4d(0.064, 0.067, 0.088, 0.251) low : Vector4d(0.064, 0.067, 0.088, 0.252) (0, 5) high: Vector4d(0.161, 0.168, 0.221, 0.631) low : Vector4d(0.161, 0.168, 0.221, 0.63) (0, 6) high: Vector4d(0.248, 0.26, 0.341, 0.969) low : Vector4d(0.248, 0.26, 0.341, 0.967) (0, 7) high: Vector4d(0.258, 0.271, 0.355, 1) low : Vector4d(0.258, 0.271, 0.355, 1.007) (0, 8) high: Vector4d(0.259, 0.271, 0.355, 1) low : Vector4d(0.259, 0.271, 0.355, 1) (0, 9) high: Vector4d(0.262, 0.274, 0.358, 1) low : Vector4d(0.262, 0.274, 0.358, 1) # Just a 16-bits per channel rendering -------------------------------------------------------------------------------- rendering - bmp.GetSize() = (128, 128), bmp.GetBt() = 48, bmp.GetBpz() = 768. Executing GetBitmapDataHighLevel() took 0.0091 seconds. Executing GetBitmapDataLowLevel() took 0.0124 seconds. (0, 0) high: Vector4d(0, 0, 0, 1) low : Vector4d(0, 0, 0, 1) (0, 1) high: Vector4d(0, 0, 0, 1) low : Vector4d(0, 0, 0, 1) (0, 2) high: Vector4d(0, 0, 0, 1) low : Vector4d(0, 0, 0, 1) (0, 3) high: Vector4d(0.066, 0.068, 0.083, 1) low : Vector4d(0.066, 0.068, 0.083, 1) (0, 4) high: Vector4d(0.28, 0.286, 0.328, 1) low : Vector4d(0.28, 0.286, 0.328, 1) (0, 5) high: Vector4d(0.437, 0.447, 0.507, 1) low : Vector4d(0.437, 0.447, 0.507, 1) (0, 6) high: Vector4d(0.535, 0.546, 0.619, 1) low : Vector4d(0.535, 0.546, 0.619, 1) (0, 7) high: Vector4d(0.545, 0.557, 0.63, 1) low : Vector4d(0.545, 0.557, 0.63, 1) (0, 8) high: Vector4d(0.546, 0.558, 0.631, 1) low : Vector4d(0.546, 0.558, 0.631, 1) (0, 9) high: Vector4d(0.548, 0.56, 0.633, 1) low : Vector4d(0.548, 0.56, 0.633, 1) # Just a 8-bits per channel rendering -------------------------------------------------------------------------------- rendering - bmp.GetSize() = (128, 128), bmp.GetBt() = 24, bmp.GetBpz() = 384. Executing GetBitmapDataHighLevel() took 0.0090 seconds. Executing GetBitmapDataLowLevel() took 0.0109 seconds. (0, 0) high: Vector4d(0, 0, 0, 1) low : Vector4d(0, 0, 0, 1) (0, 1) high: Vector4d(0, 0, 0, 1) low : Vector4d(0, 0, 0, 1) (0, 2) high: Vector4d(0, 0, 0, 1) low : Vector4d(0, 0, 0, 1) (0, 3) high: Vector4d(0.067, 0.071, 0.082, 1) low : Vector4d(0.067, 0.071, 0.082, 1) (0, 4) high: Vector4d(0.282, 0.286, 0.329, 1) low : Vector4d(0.282, 0.286, 0.329, 1) (0, 5) high: Vector4d(0.435, 0.447, 0.506, 1) low : Vector4d(0.435, 0.447, 0.506, 1) (0, 6) high: Vector4d(0.537, 0.545, 0.62, 1) low : Vector4d(0.537, 0.545, 0.62, 1) (0, 7) high: Vector4d(0.545, 0.557, 0.631, 1) low : Vector4d(0.545, 0.557, 0.631, 1) (0, 8) high: Vector4d(0.549, 0.557, 0.631, 1) low : Vector4d(0.549, 0.557, 0.631, 1) (0, 9) high: Vector4d(0.549, 0.561, 0.631, 1) low : Vector4d(0.549, 0.561, 0.631, 1)
Code:
"""Demonstrates low- and high-level pixel data access with type BaseBitmap, including alpha channel data. To take full effect of this script you must open the file from within the provided directory, so that the script can find the example files next to it. The script demonstrates what I dubbed here high- and low-level BaseBitmap access, using BaseBitmap.GetPixelDirect (high) and BaseBitmap.GetPixelCnt (low). It is not advisable to use low-level access, as it (slightly) looses out in performance on high-level access in most cases and is much more complicated to write and handle. The reason is to get the low-level access somewhat performant, we must read data line-by-line and deal manually with the pixel formats. When data is read pixel-by-pixel with low-level access, the code will be much cleaner but also MUCH slower than the built in high-level and also pixel by pixel access methods. This is because Pythons struct library is very slow. Note: * This code example has turned out much more technical and dense than I am usually comfortable with but that is unfortunately unavoidable. When you are on S22+, just look at GetBitmapDataHighLevel() it is very straight forward, is able to deal with any pixel format, and in most cases also the most performant solution. * BaseBitmap is a bit messy at the moment, the reason is that it is only and adapter these days for the underlying Image API. In C++ one can get the underlying Image API data with BaseBitmap::GetImageRef() which makes things less finicky. In Python this is currently not possible as the Image API has not been wrapped there. """ import c4d import itertools import functools import os import struct import time import typing # Maps render data bit depth symbols to their respective number of total bits per pixel. FORMATDEPTH_TO_BITS: dict[int, int] = { c4d.RDATA_FORMATDEPTH_8: 8 * 3, c4d.RDATA_FORMATDEPTH_16: 16 * 3, c4d.RDATA_FORMATDEPTH_32: 32 * 3, } # Maps Image API byte sizes per pixel to the respective color modes. COLOR_BYTES_TO_MODES: dict[int, int] = { c4d.COLORBYTES_RGB: c4d.COLORMODE_RGB, c4d.COLORBYTES_ARGB: c4d.COLORMODE_ARGB, c4d.COLORBYTES_RGBw: c4d.COLORMODE_RGBw, c4d.COLORBYTES_ARGBw: c4d.COLORMODE_ARGBw, c4d.COLORBYTES_RGBf: c4d.COLORMODE_RGBf, c4d.COLORBYTES_ARGBf: c4d.COLORMODE_ARGBf, } # Translates relevant pixel formats to packing strings used by Python's #struct. COLOR_BYTES_TO_PACK_STRING: dict = { # COLORBYTES_RGB is an alias for maxon::PIX which is an alias for unsigned char. c4d.COLORBYTES_RGB: "B", c4d.COLORBYTES_ARGB: "B", # COLORBYTES_RGBw is an alias for maxon::PIX_W which is an alias for unsigned short/Int16. c4d.COLORBYTES_RGBw: "H", c4d.COLORBYTES_ARGBw: "H", # COLORBYTES_RGBw is an alias for maxon::PIX_F which is an alias for a single/32-bit float. c4d.COLORBYTES_RGBf: "f", c4d.COLORBYTES_ARGBf: "f", } # The active document. doc: c4d.documents.BaseDocument # Type alias for bitmap data stored as a dictionary of RGBA values over coordinate keys. BitmapData: typing.Type = dict[tuple[int, int], c4d.Vector4d] def TimeIt(func): """Provides a makeshift decorator for timing functions executions. """ @functools.wraps(func) def wrapper(*args, **kwargs): """Wraps and measures the execution time of the wrapped function. """ t0: int = time.perf_counter() result: typing.Any = func(*args, **kwargs) print(f"Executing {func.__name__}() took {(time.perf_counter() - t0):.4f} seconds.") return result return wrapper def RenderImage(doc: c4d.documents.BaseDocument, size: tuple[int]) -> c4d.bitmaps.BaseBitmap: """Renders the passed document into a BaseBitmap with the given #size. Does not ensure that the image/rendering is 32bits per channel. """ data: c4d.BaseContainer = doc.GetActiveRenderData().GetData() data[c4d.RDATA_XRES] = size[0] data[c4d.RDATA_YRES] = size[1] bitFormat: int = data[c4d.RDATA_FORMATDEPTH] bmp: c4d.bitmaps.BaseBitmap = c4d.bitmaps.BaseBitmap() bmp.Init(size[0], size[1], FORMATDEPTH_TO_BITS[bitFormat]) res: int = c4d.documents.RenderDocument(doc, data, bmp, c4d.RENDERFLAGS_EXTERNAL) if res != c4d.RENDERRESULT_OK: raise RuntimeError(f"Could not render {doc}.") return bmp def LoadImages(path: str, format:str = ".psd") -> list[tuple[str, c4d.bitmaps.BaseBitmap]]: """Loads the images in #path with the given #format as BaseBitmap instances. """ if not isinstance(path, str) or not os.path.exists(path): raise IOError() result: list[c4d.bitmaps.BaseBitmap] = [] for fileName in os.listdir(path): file, ext = os.path.splitext(fileName) if not os.path.isfile(fileName) or ext.lower() != format: continue filePath: str = os.path.join(path, fileName) bmp: c4d.bitmaps.BaseBitmap = c4d.bitmaps.BaseBitmap() res, _ = bmp.InitWith(filePath) if res != c4d.IMAGERESULT_OK: raise IOError() result.append((fileName, bmp)) return result @TimeIt def GetBitmapDataHighLevel(bmp: c4d.bitmaps.BaseBitmap) -> BitmapData: """Returns the bitmap data of #bmp via the high level methods BaseBitmap.GetPixelDirect and GetAlphaPixel. GetPixelDirect supports bit depths higher than 8-bit although its value lie in the typical 8-bit [0, 255] interval. The values are however floating point and not integer. """ size: list[int] = bmp.GetSize() alphaChannel: typing.Optional[c4d.bitmaps.BaseBitmap] = bmp.GetInternalChannel() data: BitmapData = {} f: float = 1. / 255. for x, y in itertools.product(range(size[0]), range(size[1])): rgb: c4d.Vector = bmp.GetPixelDirect(x, y) a: float = bmp.GetAlphaPixel(alphaChannel, x, y) * f if alphaChannel else 1. data[(x, y)] = c4d.Vector4d(rgb.x * f, rgb.y * f, rgb.z * f, a) return data @TimeIt def GetBitmapDataLowLevel(bmp: c4d.bitmaps.BaseBitmap) -> BitmapData: """Returns the bitmap data of #bmp via direct memory access. In C++, such low level direct memory access is much faster, but as usually the case such direct and raw memory access does not translate well to Python. Even in its most optimized form, it will be slower than high level access, it is therefore recommended to use GetPixelDirect and GetAlphaPixel when possible. The major culprit is the `struct` library of Python which is very slow. Note: We could also technically ignore the source format here and always pack everything into 32-bits as #dstmode of #GetPixelCnt denotes the destination format and not the source format. I instead showed here how to deal with each format in its native form. """ width, height = bmp.GetSize() data: BitmapData = {} # Determine if the bitmap has an internal alpha channel or not. Note that other than in the # Image API, a BaseBitmap returns many values as if it had three channels, even when there is # an alpha channel as the fourth channel per pixel. alphaChannel: typing.Optional[c4d.bitmaps.BaseBitmap] = bmp.GetInternalChannel() chanelCount: int = 4 if alphaChannel else 3 # The bits and bytes per pixel and the size of a line buffer. bitsPerPixel: int = bmp.GetBt() bytesPerPixel: int = bitsPerPixel // 8 bufferSize: int = bmp.GetBpz() # equal to bytesPerPixel * width # E.g. for 32 bits per channel and a width of 128: # bitsPerPixel = 96, bytesPerPixel = 12, bytesPerPixel * width = 1536 , bufferSize = 1536 # As described above, the bitmap always acts as if it has three channels when it comes to # its #bitsPerPixel, and while we MUST traverse the data with a stride that includes the # alpha channel, there is no meaningful data to be found there. colorBytes: int = int((bytesPerPixel / 3) * chanelCount) colorMode: int = COLOR_BYTES_TO_MODES[colorBytes] bufferSize: int = colorBytes * width rgbBuffer: bytearray = bytearray(bufferSize) # Because we are unable to read the alpha channel implicitly, we require a second buffer. if alphaChannel: alphaBuffer: bytearray = bytearray(bufferSize) # Pick the correct unpacking format corresponding to the maxon:PIX_? format referenced by the # given #colorBytes. See #COLOR_BYTES_TO_PACK_STRING at the top of the file. formatString: str = COLOR_BYTES_TO_PACK_STRING[colorBytes] * (chanelCount * width) # Conversion factors for converting 8- and 16-bit values to a floating point representation. f8: float = 1. / 255. f16: float = 1. / 65535. def GetLineData(data: c4d.bitmaps.BaseBitmap, y: int, buffer: bytearray) -> list[float]: """Abstracts reading one line of pixels into a floating point format independent of the source format. """ # Read the line #y into the line buffer. data.GetPixelCnt(0, y, width, buffer, colorBytes, colorMode, c4d.PIXELCNT_NONE) # Unpack the line into its native form, Uchar (8bit), Ushort (16bit), or single (32bit). result: list[typing.Any] = struct.unpack(formatString, buffer) # Doing this is optional, but I convert here the 8 bit and 16 bit integer data to a floating # point format, I do this primarily because that is what is being done by the high level # access methods. if colorBytes in (c4d.COLORBYTES_RGB, c4d.COLORBYTES_ARGB): result = [n * f8 for n in result] if colorBytes in (c4d.COLORBYTES_RGBw, c4d.COLORBYTES_ARGBw): result = [n * f16 for n in result] return result # Read and unpack data line by line. When we access data pixel by pixel instead, access will be # very slow, the culprit is here primarily #struct which is notorious for being very slow. This # only applies to Python, in C++ low level access will be much faster than high level access. for y in range(height): # Unpack a line of RGB data and optionally alpha data when necessary. rgbRaw: list[float] = GetLineData(bmp, y, rgbBuffer) if alphaChannel: alphaRaw: list[float] = GetLineData(alphaChannel, y, alphaBuffer) # Convert one line into RGBA vectors stored over their pixel coordinate. for x, offset in enumerate(range(0, len(rgbRaw), chanelCount)): data[(x, y)] = c4d.Vector4d(rgbRaw[offset+1 if alphaChannel else offset], rgbRaw[offset+2 if alphaChannel else offset+1], rgbRaw[offset+3 if alphaChannel else offset+2], alphaRaw[offset+1] if alphaChannel else 1.) return data def main() -> None: """Runs the example. """ # Clear the console and get the input images. The images loaded by LoadImages() are expected # to lie next to this script. c4d.CallCommand(13957) imageData: list[tuple[str, c4d.bitmaps.BaseBitmap]] = ( [("rendering", RenderImage(doc, (128, 128)))] + LoadImages(os.path.dirname(__file__))) # Iterate over the images, access their data low- and high-level data, and print out timings # and results. High level access wins pretty much in every case, and low-level access is much # more cumbersome, I would avoid dealing with it when possible. for label, bmp in imageData: print("\n" + ("-" * 80)) print(f"{label} - {bmp.GetSize() = }, {bmp.GetBt() = }, {bmp.GetBpz() = }.") highLevelData: BitmapData = GetBitmapDataHighLevel(bmp) lowLevelData: BitmapData = GetBitmapDataLowLevel(bmp) for key in list(highLevelData.keys())[0:10]: print (f"{key} high: {highLevelData[key]}\n" f"{' ' * (len(str(key)) + 1)}low : {lowLevelData[key]}") if __name__ == "__main__": main()
- I will update
-
Thank you Ferdinand for your time,
and also providing a high / low level solution.As far as I can tell it works all as intended - my code now results in clean colors "measured". (in lowlvl R20 and highlvl 2023)
I found a small discrepancy with render instances but that's another topic.
Cheers,
mogh -
-