Writing/Reading Rendered Image to and from Text
-
Hello,
I am trying to write/read a C4D-rendered image file to & from a string. In this example, I am able to use Base64 to write the image but I have to save to disk in order to convert it to a string. The same goes for reading it back from a string to aBaseBitmap
.- How can I go straight from the rendered bitmap in memory to a string without having to save to disk?
- How can I read the string back into memory and initialize a
BaseBitmap
without having to save the image again to disk as I'm doing here?
import c4d, os, base64 from c4d import storage desktop = storage.GeGetC4DPath(c4d.C4D_PATH_DESKTOP) rd = doc.GetActiveRenderData() imageString = "" def ReadImage(b64str): image_64_decode = base64.decodestring(b64str) bmp2Path = os.path.join(desktop,"Test.jpg") with open(bmp2Path, "rb") as imageFile: imageString = base64.b64encode(imageFile.read()) print imageString image_result = open(bmp2Path, 'wb') image_result.write(image_64_decode) bmp = c4d.bitmaps.BaseBitmap() bmp.InitWith(bmp2Path) def WriteImage(bmp,bmpPath): if bmp.Save(bmpPath, c4d.FILTER_JPG) == c4d.IMAGERESULT_OK: print "Bitmap saved to: %s"%bmpPath with open(bmpPath, "rb") as imageFile: imageString = base64.b64encode(imageFile.read()) return imageString return False def main(doc): bmp = c4d.bitmaps.BaseBitmap() bmp.Init(250,250,24) bmpPath = os.path.join(desktop,"Test.jpg") if c4d.documents.RenderDocument(doc, rd.GetData(), bmp, c4d.RENDERFLAGS_EXTERNAL) != c4d.RENDERRESULT_OK: raise RuntimeError("Failed to render the document.") else: imgString = WriteImage(bmp,bmpPath) ReadImage(imgString) if __name__=='__main__': main(doc)
Thank you.
-
Hi,
I am a bit confused. In your example you are serialising your image as a JPEG to disk. Cinema's bitmap format is obviously a raw and lossless one, so there is that discrepancy for once.
While Python offers with
pickle
andmarshal
two modules for byte-serialisation and withjson
one for string serialisation, the language lacks the capability to serialise arbitrary objects like you can for example in C# viaXmlSerializer
(but even there you have to fulfil certain criteria and cannot just serialise anything).So you will have to come up with your own serialisation scheme, since
BaseBitmap
does not have its own format. There is alsoBaseBitmap.SetPixelCnt
which lets you write from a byte buffer into the bitmap, but this is only a performance feature, you can also just serialise and deserialise your data pixel by pixel.For the serialisation: There are no inherently wrong ways to do this. It is common practice to reserve a fixed amount of bytes for the header, where you put the important metadata of the file (for a bitmap that could be the bit depth and the number of pixel rows and columns). Then you have to define the data in the body. For an uncompressed bitmap that could be simply enumerating the RGB triples. You could look at other formats, but file formats, even the simple ones, are usually quite complex. If you want to include multiple layers and/or alpha channels, things would get more complicated, as you would not only have to serialise the additional bitmap data, but also the relation they are in.
Cheers,
zipit -
@zipit Thank you for the reply and info regarding byte-serialisation. Yes, you're right about the serialising my image as a JPG to disk being confusing because, as I mentioned in the original post, I don't know how to write to text or read it back in Cinema 4D without saving it as an image (in this case) with
base64
. I'm aware this is not the way to do it & want to skip the image step in both functions entirely by getting the bitmap data from theBaseBitmap
(Γ la the Pythonread
method for reading bytes from a binary file).My images will not have alpha channels or metadata. I tried what you mentioned with writing the RGB of each pixel and the text file was 800KB for a 250x250px image compared to the 6KB .jpg! I had no idea the difference would be so great.
pixels = list() for wPixel in xrange(bmp.GetBw()): for hPixel in xrange(bmp.GetBh()): pixels.append(bmp.GetPixel(wPixel, hPixel)) filePath = os.path.join(desktop,"Image.txt") f= open(filePath,"w") rgb_values = ','.join(str(p) for p in pixels) f.write(rgb_values) f.close()
I imagine reading 818361 characters back, converting them to lists, then writing them with SetPixel will be a slow operation? I'm guessing because of the image compression, when I saved the
base64
byte string to a text file, it was only 8KB, but again, I had to save as an image to disk first.Could I get the compressed .jpg data without saving the image first, perhaps, or is there really no way to get this data from
BaseBitmap
? Can anyone suggest a faster,smaller way of doing this? Thanks! -
Hi,
@blastframe said in Writing/Reading Rendered Image to and from Text:
... because, as I mentioned in the original post, I don't know how to write to text or read it back without saving it as an image (in this case) with base64.
You can not, because
BaseBitmap
is only being serialiseable via the offered common image formats, and this serialisation is, depending on the chosen format, also lossy (a JPEG cannot have layers for example).BaseBitmap
is a complex data type, think of it as a diagram, that has no inherent information about how to be expressed as an ordered tuple of values, i.e. how to be serialised. base64 is also just a number system, so you are just expressing the existing data in a slightly more convenient way (you could also save binary, i.e. base 2, as text, it just would be very long).Could I get the compressed .jpg data without saving the image first, perhaps, or is there really no way to get this data from
BaseBitmap
? Thanks!The lossy image formats are quite efficient at what they do, so no, you won't get things as small without using something specialised like a wavelet compression for example. Things you can do:
- Move away from string serialisation, as it is quite inefficient when it comes to most performance metrics (space, time, etc.). The purpose of string serialisation is to be human readable and to be as barrier-free as possible for data exchange. There is nothing wrong with string serialisations, I like them too, you just have to pay the costs.
- If you do not want to implement a more complex image compression algorithm or use one of the existing 3rd party Python modules, you could employ one of the all purpose compression algorithms, like for example LZW. It won't be as effective a wavelet compression, but in some cases, it will cut down the file size significantly. Python has the
zlib
module, which implements an algorithm that is very similar to LZW. The file formats GIF and TIF use LZW-compression internally.
edit: Yes,
SetPixel
is probably quite slow (never tested the performance myself, but array insertion operations are expensive, so this method is most likely quite expensive too. I mentioned in my first post the methodSetPixelCnt
to write consecutive blocks of pixels, there is even an example for it in the docs).Cheers,
zipit -
@zipit Thank you again for the thorough explanation.
The existing 3rd party Python modules to which you are referring are
pickle
andmarshal
? -
Hi again
no,
pickle
andmarshal
are 'bultin' modules of Python and both general purpose serialisation modules (pickling and marshling are both just odd synonyms for serialisation). A common candidate for such 3rd party image module would bepil
or its successorpillow
. Both libraries accept byte objects when invoking their save functionality, which would allow you to serialise something into the image format of your choice without having to go the route of saving it to disk. It would go something like that:import io from PIL import Image # The source of the bitmap would obviously different, I am just not # in the mood to install pillow in Cinema. You can create Image objects # from a byte buffer, which would allow you to instiate an Image object # from a raw pixel array fed by Cinemas BaseBitmap. image = Image.open("test.png") # We need to get rid of the alpha channel first, or pillow will complain when # we try to save the image as a JPEG. image = image.convert("RGB") # The buffer object. buffer = io.BytesIO() # We just save the image with the buffer object in place of the file path. image.save(buffer, format="JPEG") # The first 10 bytes of the image in JPEG format. print (buffer.getvalue()[:9])
Cheers,
zipit -
Hi @zipit ! Thank you again.
Yes, I saw the
pil
module, but didn't pursue it because I don't know the recommended way for including a 3rd party module with my plugins. How would you include the module? Using Niklas Rosenstein's localimport or is there another way to do it? -
Hi,
AFAIK there is no recommended way. Due to the fact the MAXON decided to rip out
pip
and not deliver a package manager with Cinema's Python, it would be quite some work to installpillow
in the first place, as it has a long list of dependencies. With that I mean in Cinema's fakesite-packages
folder that is meant for third party modules. You could also try to includepillow
locally with your plugin, but that would probably require a decent amount of monkey patching to get things to work. If you only import the module locally in the interpreter and then clean up after yourself (i.e. use Nikklas importer thingy), is probably only a detail in these considerations.If you want to do this in a plugin intended for distribution, I would look for another solution.
PS: And just to be clear. Although it says
PIL
in my example code, this is actuallypillow
code that ran on Python 3.9. The naming history of the package is not the smartest, to put it mildlyCheers,
zipit -
Hi @zipit ,
Thank you for the clarification and sorry for all the questions.What do you mean, clean up after myself? Would I need to remove something after importing it?
Yes, the plugin is meant for distribution...so you suggest I look for a solution other than
pillow
& Niklas'localimport
?Thank you.
P.S. It seems like C4D is recognizing modules in this folder for me...
C:\Program Files\Maxon Cinema 4D R22\resource\modules\python\libs\win64\python27.vs2008.framework\lib\site-packages
I'm now trying to create Image objects from a byte buffer -
Hoi again,
- With cleaning up I was just referring to what these local import modules / hacks usually do. I haven't looked at Nikklas' code, but I was implying that it was doing it for you.
- Yes, that is the path I meant.
site-packages
is normally the directory where you place 3rd party packages in the library of a Python installation. I called it fake, because Maxon moved the path out of the Python installation that comes with Cinema. Probably not the best choice of words on my end. - Yes, I would not go that route due to the fact that I seems like a lot of work. But that does not mean that it is impossible. I would be confident to find another solution, but if you are not, there is nothing inherently wrong with installing
pillow
in Cinema's Python. It will probably be just a lot of tinkering until you have come up with a custom installer, since there is nopip
in Cinema's Python. The first thing to check would be if you can drag and drop installpillow
and its dependencies you need. Python packages come sometimes with an installer that is run bypip
when you install the package via the command line and sometimes the execution of that installer is actually required for the package to work properly.
PS: Also, when you install
pillow
globally insite-packages
, you won't need Nikklas' package, since it is already sitting in the global module directory then, so it does not make sense to import it locally then.Cheers,
zipit -
@zipit Thank you for all of your guidance. You've been very helpful.
-
Hi sorry to come a bit late to the party, but you are not forced to rely on 3rd part solution you can export your BaseBitmap to a bitsequence then it's up to you to convert this bitsequence in a way that can be represented in a file.
Find an example in read_write_memory_file_bitmap_r13.
Cheers,
Maxime. -
@m_adam Thank you, Maxime.
I still learned a lot from your help, @zipit , thank you too.