libvips / pyvips

python binding for libvips using cffi
MIT License
649 stars 50 forks source link

openslideload does not return correct xres/yres on images written by pyvips #517

Closed stephandooper closed 3 days ago

stephandooper commented 3 days ago

Hi,

I was playing around with the pyvips image loaders new_from_file and openslideload. I noticed that when you write an image using pyvips and then load it using openslideload, then openslideload will always set the xres/yres metadata to 1. This happens even if it's forced during writing.

Pyvips version: 2.2.3 libvips version: 8.16 (also tested on 8.12)

image data used: I'm using normal_001.tif from the CAMELYON16 dataset: https://camelyon17.grand-challenge.org/Data/ I also found this same problem using several svs files from TCGA so it might be a wider thing


from pprint import pprint
import pyvips

def print_all_fields(image):
    fields = image.get_fields()
    result = {}

    for field in fields:
        result[field] = image.get(field)

    pprint(result)
    print("")

if __name__ == "__main__":

    image_path = "/data/pathology/archives/breast/camelyon/CAMELYON16/images/normal_001.tif"
    write_path = "/data/temporary/stephan/data/normal_001_test.tif"

    image = pyvips.Image.openslideload(image_path, rgb=True, level=4)
    print_all_fields(image)

    new_image = image.copy(xres=300, yres=300)
    new_image.write_to_file(write_path, pyramid=True, tile=True, compression="jpeg")

    #Yields xres/yres 1 which is wrong
    image = pyvips.Image.openslideload(write_path, rgb=True, level=0)
    print_all_fields(image)

    #Yields xres/yres 300 which is correct
    image = pyvips.Image.new_from_file(write_path, page=0)
    print_all_fields(image)
jcupitt commented 3 days ago

Hi @stephandooper,

Have you tried with openslide 4? I think this fails with the old openslide 3.4.1, but openslide4 ought to fix it, I think. It seems to work for me:

$ vipsheader -f xres CMU-1.svs
2004.0080160320642
$ vips crop CMU-1.svs x.tif[tile,compression=jpeg,pyramid] 0 0 1000 1000
$ vipsheader -f xres x.tif
2004.008056640625

So openslideload from a SVS works OK. If I read the same TIFF back again I see:

$ vips openslideload x.tif x.v
$ vipsheader -f xres x.v
2004.008031496063

So it has the res back from the openslide vanilla TIFF reader too.

jcupitt commented 3 days ago

(openslide4 still isn't in current ubuntu, but I think it'll be in 24.10)

stephandooper commented 3 days ago

Good call, I'm indeed using 3.4.1. I'll try openslide 4 and get back to you

jcupitt commented 3 days ago

I just updated to 24.10 and openslide is still 3.4.1 sigh.

It ought to be simple to build from source at least.

stephandooper commented 3 days ago

I managed to build openslide 4.0 without any hassle. Unfortunately the vipsheader command still returns an xres of 1, would have any idea why?

This is my build information:

openslide: openslide-show-properties --version openslide-show-properties 4.0.0, using OpenSlide 4.0.0

pyvips: enable debug: false enable deprecated: true enable modules: true enable cplusplus: true enable RAD load/save: true enable Analyze7 load: true enable PPM load/save: true enable GIF load: true FFTs with fftw3: true SIMD support with orc-0.4: true ICC profile support with lcms2: true deflate compression with zlib: true text rendering with pangocairo: true font file support with fontconfig: true EXIF metadata support with libexif: true JPEG load/save with libjpeg: true JXL load/save with libjxl: false (dynamic module: false) JPEG2000 load/save with libopenjp2: true PNG load/save with libpng: true image quantisation with imagequant: true TIFF load/save with libtiff-4: true image pyramid save with libarchive: true HEIC/AVIF load/save with libheif: false (dynamic module: false) WebP load/save with libwebp: true PDF load with poppler-glib: true (dynamic module: true) SVG load with librsvg-2.0: true EXR load with OpenEXR: true WSI load with openslide: true (dynamic module: true) Matlab load with matio: true NIfTI load/save with libnifti: false FITS load/save with cfitsio: true GIF save with cgif: false Magick load/save with MagickCore: false (dynamic module: false)

jcupitt commented 3 days ago

What do you see for meson output during configure? I have:

vips 8.17.0

  Dependencies
    glib-2.0                          : 2.82.1
    gio-2.0                           : 2.82.1
    gobject-2.0                       : 2.82.1
    gmodule-no-export-2.0             : 2.82.1
    expat                             : 2.6.2
    zlib                              : 1.3.1
    libarchive                        : 3.7.4
    fftw3                             : 3.3.10
    cfitsio                           : 4.4.1
    imagequant                        : 2.18.0
    cgif                              : 0.4.1
    libexif                           : 0.6.24
    libjpeg                           : 2.1.5
    spng                              : 0.7.4
    libwebp                           : 1.4.0
    libwebpmux                        : 1.4.0
    libwebpdemux                      : 1.4.0
    pangocairo                        : 1.54.0
    pangoft2                          : 1.54.0
    fontconfig                        : 2.15.0
    libtiff-4                         : 4.5.1
    librsvg-2.0                       : 2.59.1
    cairo                             : 1.18.2
    matio                             : 1.5.27
    lcms2                             : 2.14
    OpenEXR                           : 3.1.5
    libopenjp2                        : 2.5.0
    libhwy                            : 1.2.0
    niftiio                           : 3.0.1
    MagickCore                        : 6.9.13
    openslide                         : 4.0.0
    libheif                           : 1.18.1
    libjxl                            : 0.10.3
    libjxl_threads                    : 0.10.3
    poppler-glib                      : 24.08.0

Could pyvips be picking up an old libvips?

I made a tiny test program so we can be sure we're testing the same thing:

#!/usr/bin/env python3

import sys
import pyvips

image = pyvips.Image.new_from_file(sys.argv[1])

# take a small part of the image, to keep speed reasonable
image = image.crop(0, 0, min(1000, image.width), min(1000, image.height))

print(f"original image xres = {image.xres}")

# have to save to a file so openslide can load it
image.tiffsave("x.tif", compression="jpeg", tile=True, pyramid=True)

tiffload = pyvips.Image.tiffload("x.tif")
print(f"after tiffsave/tiffload, xres = {tiffload.xres}")

openslideload = pyvips.Image.openslideload("x.tif")
print(f"after tiffsave/openslideload, xres = {openslideload.xres}")

I see:

$ ./openslide-res.py ~/pics/openslide/CMU-1.svs
original image xres = 2004.0080160320642
after tiffsave/tiffload, xres = 2004.008056640625
after tiffsave/openslideload, xres = 2004.0080585629923
stephandooper commented 3 days ago

Okay I managed to fix it. The conclusion was that I made a rookie mistake.

I messed up the order and first compiled libvips, and then compiled openslide, so vips was still using openslide 3.4.1

After compiling in the correct order I tested everything again and openslideload now gives the correct xres and yres:

$ vipsheader -f xres normal_001.tif
4113.6349606299209$ vips crop xres normal_001.tif x.tif[tile,compression=jpeg,pyramid] 0 0 1000 1000
$ vipsheader -f xres x.tif
4113.6349606299209

Verifiying that openslide indeed loads the correct resolution:


$ vips openslideload x.tif x.v
$ vipsheader -f xres x.v
4113.6349606299209``` 

So everything works now. Thanks!
jcupitt commented 3 days ago

Great!

Did you get libdicom too? openslide4 now has reasonable DICOM slide support, which can be handy. It also supports slide colour management.

vipsdisp can be useful for viewing slides:

https://flathub.org/en-GB/apps/org.libvips.vipsdisp

https://github.com/jcupitt/vipsdisp

The README has some notes on the keybindings.

stephandooper commented 3 days ago

No, I didn't know about this, I'll check it out right away. Thanks for the heads up!

jcupitt commented 2 days ago

(if you use the flatpak build of vipsdisp, it has openslide4, libdicom, and slide colour management support built in)