libvips / pyvips

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

Is it possible to add attributes/methods to the Image class? #481

Closed stephandooper closed 4 months ago

stephandooper commented 4 months ago

Hi!

As the title suggests, I was wondering if it's possible to add attributes or methods to the pyvips.Image class. The reason why I am asking is that I am working with pathology data, and I found that if I am writing these images to tif, the information about the magnification is either only in the metadata accessible in img.get_fields(), which differs per vendor (svs, tif, mrxs, ...) or via xres

Specifically, in the following example I am reading an image with a magnification of 0,243 micron per pixels. However, the "xres" property is in dots per inch (I think?), so I have to convert this back into micron per pixels for this to work

import pyvips

img = pyvips.Image.new_from_file("normal_001.tif")
print(img.get_fields())
print(img.get("xres")) # standard format, dots per inch?
print( 1 / img.get("xres") * 1000)  # convert to micron per pixel, what we usually work with

output:

['width', 'height', 'bands', 'format', 'coding', 'interpretation', ...]
4113.63525390625  # (dots per inch)
0.24309398823106987  # output in micron per pixel

To keep things simple: I wanted to make life a little easier by adding an attribute to the Image class, so that I can fetch the information whenever I needed it, e.g. via img.spacings which would yield a list of the spacings in micron per pixels, instead of the dots per inch that img.xres would yield. Would this at all be possible? and what about methods? My hunch after tinkering around is that it's not that simple :)

If it's not directly possible, would it be possibe/recommended to create a new field that would be accesible via img.get_fields?

jcupitt commented 4 months ago

Hello @stephandooper,,

xres is always in pixels per millimetre, so yes, 1000.0 / xres ought to get you microns per pixel.

Python doesn't make it easy to add methods or properties to an existing class -- you're supposed to subclass objects to extend them. Ruby (for example) lets you extend classes in any crazy way you like, but python is much more opinionated.

libvips supports arbitrary metadata, so you could use that system. Perhaps:

image.set_type(pyvips.GValue.gdouble_type, "mpp", 180.0)

That adds a field called mpp (microns per pixel) to that image with a value of 180.0. Some time later you can fetch it with eg.:

mpp = image.get("mpp")

It'll appear in get_fields().

In my opinion, subclassing Image is probably cleaner and safer, but I'm sure there are good arguments either way.

stephandooper commented 4 months ago

Hi jcupitt,

Thanks for your prompt response. I indeed found that using image.set_type is a lot easier than directly subclassing, and I believe it will suit most of my needs for now.

That being said, I wanted to share my experience of subclassing the Image class directly in case anyone else has a similar question:

Consider the following example where I want to open a new image from a file, using the subclass:

class ExtendedImage(pyvips.Image):

    def some_silly_function(self):
        print("hello world")

    img = ExtendedImage.new_from_file("/data/temporary/stephan/normal_001.tif")
    print(img)
    print(type(img))
    print(img.some_silly_function())

Would lead to the following error:

<pyvips.Image 49152x54272 uchar, 3 bands, srgb>
<class 'pyvips.vimage.Image'>
VipsOperation: class "some_silly_function" not found

So it returns a regular pyvips.vimage.Image object, which makes sense, because img.image.new_from_file is a static method that calls pyvips.Operation.call(). If I want to change this, then I would have to alter the Operation class as well (and perhaps others), is that correct?

jcupitt commented 4 months ago

Ahem you're right, I've never tried making a functioning subclass of pyvips.Image myself.

You'd need to experiment a bit, perhaps intercepting __getattr__ and using a metaclass to control what methods get passed down to pyvips.Image and how the return result is wrapped.

stephandooper commented 4 months ago

Thanks, your advise is much appreciated. I'll try tinkering with those options! I'll close this issue for now.