Search code examples
python-imaging-librarybytesiovips

Converting PIL image to VIPS image


I'm working on some large histological images using Vips image library. Together with the image I have an array with coordinates. I want to make a binary mask which masks out the part of the image within the polygon created by the coordinates. I first tried to do this using vips draw function, but this is very inefficiently and takes forever (in my real code the images are about 100000 x 100000px and the array of polygons are very large).

I then tried creating the binary mask using PIL, and this works great. My problem is to convert the PIL image into an vips image. They both have to be vips images to be able to use the multiply-command. I also want to write and read from memory, as I believe this is faster than writing to disk.

In the im_PIL.save(memory_area,'TIFF') command I have to specify and image format, but since I'm creating a new image, I'm not sure what to put here.

The Vips.Image.new_from_memory(..) command returns: TypeError: constructor returned NULL

from gi.overrides import Vips
from PIL import Image, ImageDraw
import io

# Load the image into a Vips-image
im_vips = Vips.Image.new_from_file('images/image.tif')

# Coordinates for my mask
polygon_array = [(368, 116), (247, 174), (329, 222), (475, 129), (368, 116)]

# Making a new PIL image of only 1's
im_PIL = Image.new('L', (im_vips.width, im_vips.height), 1)

# Draw polygon to the PIL image filling the polygon area with 0's
ImageDraw.Draw(im_PIL).polygon(polygon_array, outline=1, fill=0)

# Write the PIL image to memory ??
memory_area = io.BytesIO()
im_PIL.save(memory_area,'TIFF')
memory_area.seek(0)

# Read the PIL image from memory into a Vips-image
im_mask_from_memory = Vips.Image.new_from_memory(memory_area.getvalue(), im_vips.width, im_vips.height, im_vips.bands, im_vips.format)

# Close the memory buffer ?
memory_area.close()

# Apply the mask with the image
im_finished = im_vips.multiply(im_mask_from_memory)

# Save image
im_finished.tiffsave('mask.tif')

Solution

  • You are saving from PIL in TIFF format, but then using the vips new_from_memory constructor, which is expecting a simple C array of pixel values.

    The easiest fix is to use new_from_buffer instead, which will load an image in some format, sniffing the format from the string. Change the middle part of your program like this:

    # Write the PIL image to memory in TIFF format
    memory_area = io.BytesIO()
    im_PIL.save(memory_area,'TIFF')
    image_str = memory_area.getvalue()
    
    # Read the PIL image from memory into a Vips-image
    im_mask_from_memory = Vips.Image.new_from_buffer(image_str, "")
    

    And it should work.

    The vips multiply operation on two 8-bit uchar images will make a 16-bit uchar image, which will look very dark, since the numeric range will be 0 - 255. You could either cast it back to uchar again (append .cast("uchar") to the multiply line) before saving, or use 255 instead of 1 for your PIL mask.

    You can also move the image from PIL to VIPS as a simple array of bytes. It might be slightly faster.

    You're right, the draw operations in vips don't work well with very large images in Python. It's not hard to write a thing in vips to make a mask image of any size from a set of points (just combine lots of && and < with the usual winding rule), but using PIL is certainly simpler.

    You could also consider having your poly mask as an SVG image. libvips can load very large SVG images efficiently (it renders sections on demand), so you just magnify it up to whatever size you need for your raster images.