3
$\begingroup$

I'm trying to put together a function that takes in a grayscale PIL Image and returns a bpy.types.Image (to be used a diffuse texture) but it feels slow:

First I've tried a simpler non python version:

def pil_to_image(pil_image, name='NewImage'):
    '''
    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 reading
    width = pil_image.width
    height = pil_image.height

    pil_pixels = pil_image.load()
    byte_to_normalized = 1.0 / 255.0
    num_pixels = width * height
    # setup bpy image
    channels = 4
    bpy_image = bpy.data.images.new(name, width=width, height=height)
    # bpy image has a flat RGBA array (similar to JS Canvas)
    bpy_pixels = [None] * width * height * channels

    for index in range(num_pixels):
        x = index % width
        y = index // width
        # read x,y int or tuple flip Y
        pixel = pil_pixels[x,height - y - 1]
        # convert to 1D index, taking channels(4) into account = red index
        r_index = index * 4
        # handle gray
        normalized_pixel = pixel * byte_to_normalized
        bpy_pixels[r_index]     = normalized_pixel
        bpy_pixels[r_index + 1] = normalized_pixel
        bpy_pixels[r_index + 2] = normalized_pixel
        bpy_pixels[r_index + 3] = 1.0
    # update pixels
    bpy_image.pixels = bpy_pixels
    print("pil_to_image completed in",time.time() - now,"s")
    return bpy_image

Which prints pil_to_image completed in 4.9107561111450195 s for a 4096 x 2160 image

I've tried using numpy, however the it's similarly slow:

def pil_to_image(pil_image, name='NewImage'):
    '''
    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 reading
    width = pil_image.width
    height = pil_image.height

    pil_pixels = pil_image.load()
    byte_to_normalized = 1.0 / 255.0
    num_pixels = width * height
    # setup bpy image
    channels = 4
    bpy_image = bpy.data.images.new(name, width=width, height=height)
    # bpy image has a flat RGBA array (similar to JS Canvas)
    bpy_image.pixels = (np.asarray(pil_image.convert('RGBA'),dtype=np.float32) * byte_to_normalized).ravel()
    print("pil_to_image completed in",time.time() - now,"s")
    return bpy_image

this prints pil_to_image completed in 5.018976926803589 s

Am I missing something? Is there a more efficient way of turning a PIL image into a Blender image to be used as a DiffuseBSDF texture?

Update

Thanks to @batFINGER link I could speed up the function a tad using slice notation:

def pil_to_image(pil_image, name='NewImage'):
    '''
    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)

    # convert Image 'L' to 'RGBA', normalize then flatten 
    bpy_image.pixels[:] = (np.asarray(pil_image.convert('RGBA'),dtype=np.float32) * byte_to_normalized).ravel()

    print("pil_to_image completed in",time.time() - now,"s")
    return bpy_image

which now prints: pil_to_image completed in 3.4869320392608643 s

Any tips on speeding it up further more than welcome :)

$\endgroup$
4
  • 1
    $\begingroup$ See blender.stackexchange.com/questions/3673/… $\endgroup$
    – batFINGER
    Commented Apr 5, 2020 at 4:15
  • $\begingroup$ Out of interest what is the timing without assigning to blender image pixels? $\endgroup$
    – batFINGER
    Commented Apr 5, 2020 at 12:18
  • $\begingroup$ pil_to_image completed in 0.006505489349365234 s $\endgroup$ Commented Apr 5, 2020 at 12:25
  • $\begingroup$ I appears it's pixel assignment is the slowest: converted to RGBA in 0.022275209426879883 converted to numpy array in 0.12366914749145508 numpy array normalized in 0.0631706714630127 ravel in 0.0 assigned pixels in 3.236783742904663 $\endgroup$ Commented Apr 5, 2020 at 12:35

1 Answer 1

5
$\begingroup$

Thanks for @batFINGER's reference to CodeManX's snippet this is the fastest option I got:

def pil_to_image(pil_image, name='NewImage'):
    '''
    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)

    # convert Image 'L' to 'RGBA', normalize then flatten 
    bpy_image.pixels[:] = (np.asarray(pil_image.convert('RGBA'),dtype=np.float32) * byte_to_normalized).ravel()

    print("pil_to_image completed in",time.time() - now,"s")
    return bpy_image

it was interesting to see that

bpy_image.pixels[:] = (np.asarray(pil_image.convert('RGBA'),dtype=np.float32) * byte_to_normalized).ravel()

was faster than

bpy_image.pixels = (np.asarray(pil_image.convert('RGBA'),dtype=np.float32) * byte_to_normalized).ravel()
$\endgroup$
1
  • 1
    $\begingroup$ I've noticed that pil_image is returning order of rows from top to bottom and blender had bottom to top (reversed order), so I had to add [::-1] before .ravel() $\endgroup$ Commented Dec 25, 2023 at 10:34

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .