How to efficiently convert a PIL Image to bpy.types.Image? 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 :)
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(&quot;pil_to_image completed in&quot;,time.time() - now,&quot;s&quot;)
    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()