I'm working on Python script to apply textures to objects. I need to load an image that was created using PIL via img_from_array = Image.fromarray(ARRAY). Is it possible?
- 33,070
- 10
- 35
- 79
- 23
- 4
1 Answers
EDIT: From a comment on the answer, it is clear that one more step is needed. EDIT 2: From another comment, there's apparently a better pil_to_image function.
Since you are starting from a PIL image, you need to convert the image to a Blender image. To do that, see this answer from which I take this function and modified it according to comments on this answer.
def pil_to_image(pil_image, name='NewImage', alpha=False):
'''
PIL image pixels is 2D array of byte tuple (when mode is 'RGB', 'RGBA') or byte (when mode is 'L')
bpy image pixels is flat array of normalized values in RGBA order
'''
now = time.time()
# setup PIL image conversion
width = pil_image.width
height = pil_image.height
byte_to_normalized = 1.0 / 255.0
# create new image
bpy_image = bpy.data.images.new(name, width=width, height=height, alpha=alpha)
# convert Image 'L' to 'RGBA', normalize then flatten
bpy_image.pixels.foreach_set((np.asarray(pil_image.convert('RGBA'),
dtype=np.float32)
* byte_to_normalized).ravel())
bpy_image.pack()
print("pil_to_image completed in",time.time() - now,"s")
return bpy_image
Once you've done that, the key is that images are stored in bpy.data.images and that you use them in texture images by assigning the image to a field in the texture.
Given your comment and the above function you need something like
bpy_image = pil_to_image(img_from_array, name=WHATEVER_NAME_YOU_WANT)
to perform that conversion
Here's an example that adds the image created above to the active object. It's a bit of overkill, as it creates the material. It assumes the object has no materials yet, so the new material will go into slot 0 and be applied to the entire object by default. It also assumes you want to be able to map UV coordinates for the texture.
If you have an existing material you need to add an image texture and link it to the shader.
object = bpy.context.active_object
material = bpy.data.materials.new(name=MATERIAL_NAME)
object.data.materials.append(material)
material.use_nodes = True
tree = material.node_tree
nodes = tree.nodes
texcord = nodes.new('ShaderNodeTexCoord')
mapping = nodes.new('ShaderNodeMapping')
teximage = nodes.new("ShaderNodeTexImage")
bsdf = nodes["Principled BSDF"]
tree.links.new(texcord.outputs[2], mapping.inputs[0])
tree.links.new(mapping.outputs[0], teximage.inputs[0])
tree.links.new(teximage.outputs[0], bsdf.inputs[0])
teximage.image = bpy_image
- 33,070
- 10
- 35
- 79
-
-
In my scenario image is generated using Python PIL: img_from_array = Image.fromarray(ARRAY).
And I want to assign this. Originally my script was:
face = "FRONT" mat = bpy.data.materials.new(name=face) mat.use_nodes = True bsdf = mat.node_tree.nodes["Principled BSDF"] texImage = mat.node_tree.nodes.new('ShaderNodeTexImage') texImage.image = bpy.data.images.load(PNG_FILE)
but now I need instead of loading PNG_FILE assign img_from_array
– wooh Mar 23 '22 at 18:43 -
-
1
pixels.foreach_set(data)was added in 2.83 and is faster than thepixels[:] = data. You also need to pack the image with.pack()afterwards, otherwise the image is not "saved" (see what happens if you close and reopen Blender). – scurest Mar 23 '22 at 20:56 -
1Also you need to pass the correct alpha= argument to images.new. This doesn't seem like it does anything, but it also matters for save/reload. – scurest Mar 23 '22 at 21:19
-
@scurest thanks. I don't have PIL installed, so I'd appreciate it if you'd review my changes to the
pil_to_imagefunction. Feel free to edit if I've botched addressing your comments. – Marty Fouts Mar 23 '22 at 23:44 -
Getting through script without errors but converted textures in Blender look weird. Is there a simple way to draw pby_image on screen (using Jupiter) – wooh Mar 25 '22 at 00:36
-
@MartyFouts Thanks for support so far. By "weird" look I mean bpy_image definitely does not match original at least on Blender object. Thus I'm asking if there is a possibility to "see" this bpy_image in Jupiter UI somehow? – wooh Mar 25 '22 at 18:04
-
It is possible that the pil_to_image code is broken. I don't have PIL so I can't test it. But the easiest way to look at the image is to plug the image texture into an emission shader and place that material on a plane with the same aspect ratio as the image. (Effectively doing an "Image as plane" kind of thing.) – Marty Fouts Mar 25 '22 at 19:11
-
@martyFouts Here is screenshot where you can see both images. bpy_image looks like a cropped version from very center of the pil_image:https://www.dropbox.com/s/yevq92tr4ywlvoc/Screenshot%202022-03-26%20at%2001.21.58.png?dl=0 – wooh Mar 25 '22 at 23:27
-
-
I'm pretty sure that's the conversion routine. Unfortunately, that's not my area. Perhaps you can ask @scurest to comment on my modifications to the routine. If it's not the conversion routine is it possible that you're supplying different coordinates to the two versions of the texture? – Marty Fouts Mar 25 '22 at 23:33
-
-
1Solved. Thank you for help. Issue was not not related with conversion but with vertice definition in my script. – wooh Mar 26 '22 at 22:46
-
@scurest It appears converted images are flipped vertically. Of course I can fix this by flipping PIL image prior to conversion but what would be correct syntax to solve this? – wooh Apr 11 '22 at 23:35
img.pixels.foreach_set(data)(and doimg.pack()afterwards). – scurest Mar 23 '22 at 08:08