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 by BaseBitmap
now only being a classic API adapter for an underlying maxon::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 in GetBitmapDataHighLevel
in the example) is usually preferable over what I dubbed low-level access (BaseBitmap.GetPixelCnt
access demonstrated in GetBitmapDataLowLevel
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 is c4d.COLORBYTES_RGBf
. The byte depth symbols are currently undocumented in Python but you can use them. See COLOR_BYTES_TO_MODES
in my script below for details.
Conclusion
- I will update
BaseBitmap.Init
so that it becomes clear that depth
can be 1, 4, 8, 16, 24, 32, 48, or 96
- I will make clear that
BaseBitmap.GetBt
and BaseBitmap.GetBpz
cannot be trusted when initalizing pixel buffers for BaseBitmap.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,
Ferdinand
PS: 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()