libvips / pyvips

python binding for libvips using cffi
MIT License
640 stars 49 forks source link

Save image to tif #289

Open SikangSHU opened 2 years ago

SikangSHU commented 2 years ago

Dear Professor: I consider to save images to tif, but there is an error occured as follows. I don't know how to save. I would greatly appreciate it if you could help me. This is my code:

img = pyvips.Image.new_from_file('E:\\testpicture_jupyter\\ometif_def6.tif', access='sequential')
patch_size = 512
n_across = img.width // patch_size
n_down = img.height // patch_size
x_max = n_across - 1
y_max = n_down - 1
print(x_max, y_max)

for y in range(0, n_down):
    print("row {} ...".format(y))
    for x in range(0, n_across):
        patch = img.crop(x * patch_size, y * patch_size,
                         patch_size, patch_size)
        image = pyvips.Image.arrayjoin(patch, across=img.width)
image.tiffsave("huge.tif")
 And the error occurred is:
(wsi2.py:1468): GLib-GObject-WARNING **: 19:19:47.663: value "34102784" of type 'gint' is invalid or out of range for property 'width' of  type 'gint'
jcupitt commented 2 years ago

Hi @SikangSHU,

You have:

        image = pyvips.Image.arrayjoin(patch, across=img.width)

Try n_across instead.

Why are you cropping and joining the image? I don't think you need to do this.

SikangSHU commented 2 years ago

Thanks so much for your patient answer. As I am doing a task and not familiar with pyvips, I am making some attempts.

My task is to unmix a wsi(contains 3 stains, each of which have the different color feature) into 3 images(each image represents a stain). As the wsi is so big, I don’t know how to realize by using ‘numpy’, so I turn to ‘pyvips’. Meanwhile, I meet some obstacles when using pyvips. My thinking to do the task is as follows.

Firstly,I crop small patches(512 512)from a large slide(53298 66607).

After splitting WSI into patches,I consider to do color convolution on each patches(512 512) to create 3 individual images, each of which is 512 512.

Finally, I consider to merge 3 individual images (512 512) which represents 3 individual stains back to 3 wsi images (53298 66607) which represents 3 individual stains.

My code representing my thinking is as follows. I think two mistakes may exist where I marked(@@@). Can you help me to correct them? Thank you very much!

import pyvips
import numpy as np
from numpy import linalg
from skimage.color.colorconv import _prepare_colorarray

img = pyvips.Image.new_from_file('E:\\testpicture_jupyter\\ometif_def6.tif', access='sequential')
patch_size = 512
n_across = img.width // patch_size
n_down = img.height // patch_size
x_max = n_across - 1
y_max = n_down - 1
print(x_max, y_max)

for y in range(0, n_down):
    print("row {} ...".format(y))
    for x in range(0, n_across):
        patch = img.crop(x * patch_size, y * patch_size,
                         patch_size, patch_size)

        img1 = _prepare_colorarray(patch, force_copy=True)    @@@@@
        np.maximum(img1, 1E-6, out=img1)
        X = np.array([[0.468, 0.721, 0.511], [0, 0.141, 0.99], [0.767, 0.576, 0.284]])
        X_inv = linalg.inv(X)
        y = np.log(img1)
        C = y @ X_inv

        null = np.zeros_like(C[:, :, 0])
        C_a = np.stack((C[:, :, 0], null, null), axis=-1)
        C_b = np.stack((null, C[:, :, 1], null), axis=-1)
        C_c = np.stack((null, null, C[:, :, 2]), axis=-1)

        conv_matrix = X
        log_rgb_a = C_a @ conv_matrix
        rgb1 = np.exp(log_rgb_a)
        log_rgb_b = C_b @ conv_matrix
        rgb2 = np.exp(log_rgb_b)
        log_rgb_c = C_c @ conv_matrix
        rgb3 = np.exp(log_rgb_c)

        rgb1[rgb1 < 0] = 0
        rgb1[rgb1 > 1] = 1

        rgb2[rgb2 < 0] = 0
        rgb2[rgb2 > 1] = 1

        image_CD8 = pyvips.Image.arrayjoin(rgb1, n_across=img.width)
        image_PanCK = pyvips.Image.arrayjoin(rgb2, n_across=img.width)
        image_Hema = pyvips.Image.arrayjoin(rgb3, n_across=img.width)

image_CD8.tiffsave("CD8.tif")     @@@@@
image_PanCK.tiffsave("PanCK.tif")
image_Hema.tiffsave("Hema.tif")
SikangSHU commented 2 years ago

I consider to do stain unmixing for the wsi. The picture as follows is a part of a wsi. Thank you! image

jcupitt commented 2 years ago

I made you a demo program:

#!/usr/bin/python3

import sys
import pyvips
import numpy as np

def process_tile(patch):
    """pass a pyvips image through numpy
    """
    # make a numpy array from the pyvips image
    array = np.ndarray(buffer=patch.write_to_memory(),
                       dtype=np.uint8,
                       shape=[patch.height, patch.width, patch.bands])

    # do something with the numpy array
    array = 255 - array

    # back to pyvips
    height, width, bands = array.shape
    linear = array.reshape(width * height * bands)
    new_patch = pyvips.Image.new_from_memory(linear.data, 
                                             width, height, bands, 
                                             'uchar')
    return new_patch

patch_size = 512

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

# WSI images are RGBA, with the A always 255 ... just take the first three
# bands (RGB)
img = img[0:3]

all_patches = []
for top in range(0, img.height, patch_size):
    for left in range(0, img.width, patch_size):
        width = min(patch_size, img.width - left)
        height = min(patch_size, img.height - top)
        patch = img.crop(left, top, width, height)

        print(f"processing {left}, {top} ...")
        patch = process_tile(patch)

        all_patches.append(patch)

n_across =  1 + img.width // patch_size
new_image = pyvips.Image.arrayjoin(all_patches, across=n_across)
new_image.write_to_file(sys.argv[2])

You'll need to drop your own numpy into the processing step.

However, this will run out of memory for large images, since it must have all the inputs to arrayjoin. You will probably need to reimplement colour convolution in pyvips if you need large image support. A pyvips implementation of colour convolution would be much faster, and you could skip the tiling stage.

SikangSHU commented 2 years ago

I made you a demo program:

#!/usr/bin/python3

import sys
import pyvips
import numpy as np

def process_tile(patch):
    """pass a pyvips image through numpy
    """
    # make a numpy array from the pyvips image
    array = np.ndarray(buffer=patch.write_to_memory(),
                       dtype=np.uint8,
                       shape=[patch.height, patch.width, patch.bands])

    # do something with the numpy array
    array = 255 - array

    # back to pyvips
    height, width, bands = array.shape
    linear = array.reshape(width * height * bands)
    new_patch = pyvips.Image.new_from_memory(linear.data, 
                                             width, height, bands, 
                                             'uchar')
    return new_patch

patch_size = 512

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

# WSI images are RGBA, with the A always 255 ... just take the first three
# bands (RGB)
img = img[0:3]

all_patches = []
for top in range(0, img.height, patch_size):
    for left in range(0, img.width, patch_size):
        width = min(patch_size, img.width - left)
        height = min(patch_size, img.height - top)
        patch = img.crop(left, top, width, height)

        print(f"processing {left}, {top} ...")
        patch = process_tile(patch)

        all_patches.append(patch)

n_across =  1 + img.width // patch_size
new_image = pyvips.Image.arrayjoin(all_patches, across=n_across)
new_image.write_to_file(sys.argv[2])

You'll need to drop your own numpy into the processing step.

However, this will run out of memory for large images, since it must have all the inputs to arrayjoin. You will probably need to reimplement colour convolution in pyvips if you need large image support. A pyvips implementation of colour convolution would be much faster, and you could skip the tiling stage.

Thank you for your help!

jcupitt commented 2 years ago

I'd like to try implementing colour deconvolution in libvips. Do you have a sample image I could experiment with?

SikangSHU commented 2 years ago

I'd like to try implementing colour deconvolution in libvips. Do you have a sample image I could experiment with?

Yes, but It's too big. The wsi is about 10G. How can L send it to you?

SikangSHU commented 2 years ago

I'd like to try implementing colour deconvolution in libvips. Do you have a sample image I could experiment with?

Yes, but It's too big. The wsi is about 10G. How can L send it to you?

The format of the image is 'tif'

jcupitt commented 2 years ago

Just a section would be fine. As long as it shows all three stains.

SikangSHU commented 2 years ago

Just a section would be fine. As long as it shows all three stains.

OK, the stain vector of three stains is [[0.468, 0.721, 0.511], [0, 0.141, 0.99], [0.767, 0.576, 0.284]] test4

jcupitt commented 2 years ago

This is sort-of working:

#!/usr/bin/python3

import sys
import pyvips

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

# scale to 0-1
image /= 256

# set of stains we deconvolve with
stain_vectors = pyvips.Image.new_from_array([
    [0.468, 0.000, 0.767],
    [0.721, 0.141, 0.576],
    [0.511, 0.990, 0.284]
])

# to stain space
stain_space = image.log().recomb(stain_vectors ** -1)
n_stains = stain_space.bands

# split by stain
stains = []
for i in range(n_stains):
    mask = pyvips.Image.new_from_array(
            [[0] * n_stains] * i +
            [[0] * i + [1] + [0] * (n_stains - i - 1)] +  
            [[0] * n_stains] * (n_stains - i - 1))
    stains.append(stain_space.recomb(mask))

# from stain space back to a set of separated RGB Images
images = [x.recomb(stain_vectors).exp() for x in stains]

for i in range(len(images)):
    filename = f"stain-{i}.tif"
    print(f"writing {filename} ...")
    (256 * images[i]).cast("uchar").write_to_file(filename)

To make:

stain-0 stain-1 stain-2

Though it doesn't look quite right.

SikangSHU commented 2 years ago

This is sort-of working:

#!/usr/bin/python3

import sys
import pyvips

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

# scale to 0-1
image /= 256

# set of stains we deconvolve with
stain_vectors = pyvips.Image.new_from_array([
    [0.468, 0.000, 0.767],
    [0.721, 0.141, 0.576],
    [0.511, 0.990, 0.284]
])

# to stain space
stain_space = image.log().recomb(stain_vectors ** -1)
n_stains = stain_space.bands

# split by stain
stains = []
for i in range(n_stains):
    mask = pyvips.Image.new_from_array(
            [[0] * n_stains] * i +
            [[0] * i + [1] + [0] * (n_stains - i - 1)] +  
            [[0] * n_stains] * (n_stains - i - 1))
    stains.append(stain_space.recomb(mask))

# from stain space back to a set of separated RGB Images
images = [x.recomb(stain_vectors).exp() for x in stains]

for i in range(len(images)):
    filename = f"stain-{i}.tif"
    print(f"writing {filename} ...")
    (256 * images[i]).cast("uchar").write_to_file(filename)

To make:

stain-0 stain-1 stain-2

Though it doesn't look quite right.

Dear Professor, thank you! I implement the code using a 10GB tif file and it runs successfully. But 3 tiff files after doing colour deconvolution seem to be too big when I open in the Qupath. How can I make some modifications on your code to convert it to the pyramid file format?

error
jcupitt commented 2 years ago

Just save as a pyramidal tiff. Try:

    filename = f"stain-{i}.tif[tile,compression=jpeg,pyramid]"
jcupitt commented 2 years ago

Slightly improved version (now uses scrgb space):

#!/usr/bin/python3

import sys
import pyvips

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

# transform to linear 0-1
image = image.colourspace("scrgb")

# set of stains we deconvolve with
stain_vectors = pyvips.Image.new_from_array([
    [0.468, 0.000, 0.767],
    [0.721, 0.141, 0.576],
    [0.511, 0.990, 0.284]
])

# to stain space
stain_space = image.log().recomb(stain_vectors ** -1)
n_stains = stain_space.bands

# split by stain
stains = [stain_space.recomb(pyvips.Image.new_from_array(
            [[0] * n_stains] * i + 
            [[0] * i + [1] + [0] * (n_stains - i - 1)] + 
            [[0] * n_stains] * (n_stains - i - 1)))
          for i in range(n_stains)]

# from stain space back to a set of separated scrgb Images
images = [x.recomb(stain_vectors).exp() for x in stains]

# back to srgb
images = [x.colourspace("srgb") for x in images]

for i in range(len(images)):
    filename = f"stain-{i}.tif"
    print(f"writing {filename} ...")
    images[i].write_to_file(filename)
SikangSHU commented 2 years ago

Thanks so much for your patient answers! I have learnt a lot from the code you give and it runs successfully on my computer. While I think the method is right, the result of the stain unmixing seems to be not good. Some colors(CD8, black)(PanCK, yellow) exist on some places that should not have these colors. For example, almost the whole picture of the colour unmixing result of PanCK is yellow while only a few places are yellow in the original image. The result of my stain unmixing is as follows.

  1. original picture original picture
  2. CD8, black CD8_black
  3. PanCK PanCK_yellow
  4. Hematoxylin Hema_blue
SikangSHU commented 2 years ago

I consider to do stain unmixing for the wsi. The picture as follows is a part of a wsi. Thank you! image

This is the result I do with numpy on a section. I think maybe this is the ideal result. I don't know why are the results using pyvips seem to be not good.

SikangSHU commented 2 years ago

Dear professor: I think you may do stain unmixing on one channel for three times while there are three channels. (Or maybe my guess is wrong

jcupitt commented 2 years ago

I agree, it's not working well and I'm not sure why. I might take another look later today if I have time.

SikangSHU commented 2 years ago

I agree, it's not working well and I'm not sure why. I might take another look later today if I have time.

Dear Professor: I think the inverse process may be wrong: (stain_vectors ** -1)

This is the process of my debugging, and now_a1 and last_a1 are different:

a1 = np.array([
    [0.468, 0.023, 0.767],
    [0.721, 0.221, 0.576],
    [0.511, 0.975, 0.284]
])
now_a1 = a1 ** -1
last_a1 = linalg.inv(a1)

print(now_a1)
print(last_a1)
jcupitt commented 2 years ago

You're right, there's something funny about the pyvips matrix invert. If works if you use the numpy invert:

#!/usr/bin/python3

import sys
import pyvips
import numpy as np
from numpy import linalg

stain = [
    [0.468, 0.023, 0.767],
    [0.721, 0.141, 0.576],
    [0.511, 0.990, 0.284]
]
stain_inverse = linalg.inv(stain).tolist()

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

# transform to linear 0-1
image = image.colourspace("scrgb")

# to stain space
stain_space = image.log().recomb(stain_inverse)
n_stains = stain_space.bands

# split by stain
stains = [stain_space.recomb(pyvips.Image.new_from_array(
            [[0] * n_stains] * i + 
            [[0] * i + [1] + [0] * (n_stains - i - 1)] + 
            [[0] * n_stains] * (n_stains - i - 1)))
          for i in range(n_stains)]

# from stain space back to a set of separated scrgb Images
images = [x.recomb(stain).exp() for x in stains] 

# back to srgb
images = [x.colourspace("srgb") for x in images]

for i in range(len(images)):
    filename = f"stain-{i}.png"
    print(f"writing {filename} ...")
    images[i].write_to_file(filename)

I see:

$ ./colourdeconv.py ~/pics/stain_sample.png 
writing stain-0.png ...
writing stain-1.png ...
writing stain-2.png ...

Generating:

stain-0 stain-1 stain-2

Which now looks correct. I'll check the pyvips inverter,

SikangSHU commented 2 years ago

Hello! I need to add a fluorescent color to the image, so I'd like to convert the three channels rgb image(tiff file) to one channel. More specifically, I need to add a fluorescent color to the first image(tiff file) so that its effect can be the same as that in the second image. I appreciate it very much if you can give me some suggestions! image image

jcupitt commented 2 years ago

These are OME-TIFFs, right? I would make two one-band images and attach some XML metadata saying that the first is a stain and the second is a florescence image.

You probably have an image like this already -- have a look at the metadata on that to see what you need.

SikangSHU commented 2 years ago

These are OME-TIFFs, right? I would make two one-band images and attach some XML metadata saying that the first is a stain and the second is a florescence image.

You probably have an image like this already -- have a look at the metadata on that to see what you need.

Yes, these are OME-TIFF. I'd like to get three one-band images and then make them to one-band florescence images respectively. When I use 'vipsheader -a' to have a look at the metadata on the florescence image what I want, I can only get some properties but can't get color information. How can I look at the metadata about that? (I tried and found it that I can't send two tiff file in github, so I send two pictures. The first one is the gray image of a stain with rgb three bands and the second one is one-band florescence image that I want.)

image image image

jcupitt commented 2 years ago

You need the image-description item. Try eg:

$ vipsheader -f image-description LuCa-7color_Scan1.ome.tiff 
<?xml version="1.0" encoding="UTF-8"?><!-- Warning: this comment is an OME-XML metadata block, which contains crucial dimensional parameters and other important metadata. Please edit cautiously (if at all), and back up the original data before doing so. For more information, see the OME-TIFF web site: https://docs.openmicroscopy.org/latest/ome-
...
SikangSHU commented 2 years ago

`(learn) C:\Users\91765>vipsheader -f image-description E:\testpicture_jupyter\channel\1.tif

(vipsheader:3340): VIPS-WARNING **: 22:20:58.807: Unknown field with tag 33560 (0x8318) encountered

(vipsheader:3340): VIPS-WARNING **: 22:20:58.817: Unknown field with tag 33560 (0x8318) encountered

(vipsheader:3340): VIPS-WARNING **: 22:20:58.818: Unknown field with tag 33560 (0x8318) encountered

(vipsheader:3340): VIPS-WARNING **: 22:20:58.820: Unknown field with tag 33560 (0x8318) encountered

(vipsheader:3340): VIPS-WARNING **: 22:20:58.822: Unknown field with tag 33560 (0x8318) encountered

(vipsheader:3340): VIPS-WARNING **: 22:20:58.824: Unknown field with tag 33560 (0x8318) encountered

(vipsheader:3340): VIPS-WARNING **: 22:20:58.828: Unknown field with tag 33560 (0x8318) encountered

(vipsheader:3340): VIPS-WARNING **: 22:20:58.833: Unknown field with tag 33560 (0x8318) encountered <?xml version="1.0" encoding="UTF-8" standalone="yes"?>

2021-11-19T02:48:51Z 2021-11-19T02:48:51Z Slide `
jcupitt commented 2 years ago

Right, but you need to set it to some XML to say what kind of images you have. Take a look at an OME-TIFF image that has the properties you need, or read the OME-TIFF documentation.

SikangSHU commented 2 years ago

OK. Thank you for your patient answering. This is the information. image

jcupitt commented 2 years ago

You'll need to design some XML based on that, then set that as the image description of the TIFF you write. Eg.:

final_image = ...
final_image.set_type(pyvips.GValue.gstr_type, "image-description",
f"""<?xml version="1.0" encoding="UTF-8"?>
...
""")
final_image.write_to_file("my-ome-image.tiff")

Have you found image.sc? It's a good place to ask OME-TIFF questions:

https://forum.image.sc

The QuPath developers all post there.

SikangSHU commented 2 years ago

You'll need to design some XML based on that, then set that as the image description of the TIFF you write. Eg.:

final_image = ...
final_image.set_type(pyvips.GValue.gstr_type, "image-description",
f"""<?xml version="1.0" encoding="UTF-8"?>
...
""")
final_image.write_to_file("my-ome-image.tiff")

Have you found image.sc? It's a good place to ask OME-TIFF questions:

https://forum.image.sc

The QuPath developers all post there.

OK, I know about that forum. I am very sincerely grateful to you for your help!

SikangSHU commented 2 years ago

Hi, I’d like to get a pyramidal tiff with which the colourspace is ‘grey16’(As shown in the figure, <pyvips.Image 66607*53298 ushort, 1 bands, grey16>). When I write like that in my code, the output image ‘gray1_1D becomes ‘<pyvips.Image 66607x53298 uchar, 1 bands, b-w>’. How can I modify my code so that the output image can be a pyramidal tiff with which the colourspace is ‘grey16’? Thank you!

image

mask1 = pyvips.Image.new_from_array(
    [[1, 0, 0],
     [0, 0, 0],
     [0, 0, 0]]
)
gray1 = stain_space_exp.recomb(mask1)
gray1_1D = gray1[0]
filename = f"gray1_1D.tif[tile,compression=jpeg,pyramid]"
print(f"writing {filename} ...")
(gray1_1D * 255).cast("int").colourspace("grey16").write_to_file(filename)
jcupitt commented 2 years ago

JPEG compression only supports 8-bit images. Try:

$ vips colourspace k2.jpg x.tif[tile,pyramid,compression=zstd] grey16
$ vipsheader x.tif
x.tif: 1450x2048 ushort, 1 band, grey16, tiffload

Of course, your files will be very large without lossy compression.

jcupitt commented 2 years ago

Ah jpeg2000 compression works with ushort data.

$ vips colourspace k2.jpg x.tif[tile,pyramid,compression=jp2k] grey16
SikangSHU commented 2 years ago

Thank you! But I meet some problems as I use windows. (learn) C:\Users\91765>vips colourspace E:\Learning\Graduate\Python_task\gray1_1D.tif E:\Learning\Graduate\Python_task\gray1_1D2.tif[tile,pyramid,compression=jp2k] grey16 tiffsave: enum 'VipsForeignTiffCompression' has no member 'jp2k', should be one of: none, jpeg, deflate, packbits, ccittfax4, lzw, webp, zstd

(learn) C:\Users\91765>vips colourspace E:\Learning\Graduate\Python_task\gray1_1D.tif E:\Learning\Graduate\Python_task\gray1_1D2.tif[tile,pyramid,compression=zstd] grey16 TIFFSetField: E:\Learning\Graduate\Python_task\gray1_1D2.tif: Unknown pseudo-tag 65564 TIFFSetField: C:\Users\91765\AppData\Local\Temp\vips-0-2730079153.tif: Unknown pseudo-tag 65564 TIFFSetField: C:\Users\91765\AppData\Local\Temp\vips-1-2774728704.tif: Unknown pseudo-tag 65564 TIFFSetField: C:\Users\91765\AppData\Local\Temp\vips-2-519427855.tif: Unknown pseudo-tag 65564 TIFFSetField: C:\Users\91765\AppData\Local\Temp\vips-3-480366557.tif: Unknown pseudo-tag 65564 TIFFSetField: C:\Users\91765\AppData\Local\Temp\vips-4-2412294859.tif: Unknown pseudo-tag 65564 TIFFSetField: C:\Users\91765\AppData\Local\Temp\vips-5-2408159456.tif: Unknown pseudo-tag 65564 TIFFSetField: C:\Users\91765\AppData\Local\Temp\vips-6-1947089405.tif: Unknown pseudo-tag 65564 TIFFSetField: C:\Users\91765\AppData\Local\Temp\vips-7-3130116404.tif: Unknown pseudo-tag 65564 TIFFSetField: C:\Users\91765\AppData\Local\Temp\vips-8-522679398.tif: Unknown pseudo-tag 65564 TIFFSetField: C:\Users\91765\AppData\Local\Temp\vips-9-1296517416.tif: Unknown pseudo-tag 65564 E:\Learning\Graduate\Python_task\gray1_1D2.tif: ZSTD compression support is not configured vips2tiff: TIFF write tile failed wbuffer_write: write failed

jcupitt commented 2 years ago

You need the -all ZIP for jpeg2000 compression:

https://github.com/libvips/build-win64-mxe/releases/download/v8.12.1/vips-dev-w64-all-8.12.1.zip

SikangSHU commented 2 years ago

[tile,pyramid,compression=jp2k]

I think your code has converted the image to pyramidal file format, but the error still exists. When I had this problem before, I converted it to pyramidal file format and the error would be solved. image

jcupitt commented 2 years ago

You could try adding subifd, QuPath might prefer it.

SikangSHU commented 2 years ago

You'll need to design some XML based on that, then set that as the image description of the TIFF you write. Eg.:

final_image = ...
final_image.set_type(pyvips.GValue.gstr_type, "image-description",
f"""<?xml version="1.0" encoding="UTF-8"?>
...
""")
final_image.write_to_file("my-ome-image.tiff")

Have you found image.sc? It's a good place to ask OME-TIFF questions:

https://forum.image.sc

The QuPath developers all post there.

Hi,I wrote the code in this way to create a one-band image after referring to the code yo gave. gray1_1D = gray1[0] filename = f"image.tif[tile,pyramid,subifd]" print(f"writing {filename} ...") (255 - gray1_1D * 255).cast("int").colourspace("grey16").write_to_file(filename) image

Now, I’d like to edit the metadata to set colour information(only need to change a colour) for the image. After setting, I’d like to get the effect as the following picture is shown. image I have tried in this way but found it that the metadata can't be changed. final_image =pyvips.Image.tiffload('image.ome.tif') final_image.set_type(pyvips.GValue.gstr_type, "image-description", f"""<?xml version="1.0" encoding="UTF-8"?> ... <Channel Color="-16718848" ID="Channel:0:0" Name="Channel 1" SamplesPerPixel="1"> ... """) final_image.write_to_file("new-image.tif") What do I need to do to edit the metadata about colours of ome-tif file? Thank you!

jcupitt commented 2 years ago

It should work. I see:

#!/usr/bin/python3

import sys
import pyvips

image = pyvips.Image.new_from_file(sys.argv[1])
image.set_type(pyvips.GValue.gstr_type, "image-description", "hello!")
image.write_to_file(sys.argv[2])

I can run it like this:

$ ./tiffmeta.py ~/pics/k2.jpg x.tif
$ vipsheader -a x.tif
x.tif: 1450x2048 uchar, 3 bands, srgb, tiffload
width: 1450
height: 2048
bands: 3
format: uchar
coding: none
interpretation: srgb
xoffset: 0
yoffset: 0
xres: 2.835
yres: 2.835
filename: x.tif
vips-loader: tiffload
n-pages: 1
image-description: hello!
resolution-unit: in
orientation: 1

Post a complete program I can run that shows the problem you are having.

SikangSHU commented 2 years ago

It should work. I see:

#!/usr/bin/python3

import sys
import pyvips

image = pyvips.Image.new_from_file(sys.argv[1])
image.set_type(pyvips.GValue.gstr_type, "image-description", "hello!")
image.write_to_file(sys.argv[2])

I can run it like this:

$ ./tiffmeta.py ~/pics/k2.jpg x.tif
$ vipsheader -a x.tif
x.tif: 1450x2048 uchar, 3 bands, srgb, tiffload
width: 1450
height: 2048
bands: 3
format: uchar
coding: none
interpretation: srgb
xoffset: 0
yoffset: 0
xres: 2.835
yres: 2.835
filename: x.tif
vips-loader: tiffload
n-pages: 1
image-description: hello!
resolution-unit: in
orientation: 1

Post a complete program I can run that shows the problem you are having.

Thank you for your patient answering! This is what my program runs: image

No matter is final_image.write_to_file(single_inverse2.ome.tif]) or final_image.write_to_file(single_inverse2.ome.tif[tile,pyramid,subifd]), the image-description can't be editted. Only the situation as follows can the image description be changed, but this format isn't what I need. image

jcupitt commented 2 years ago

Don't post screenshots -- I can't copy-paste the code out to run it.

Post some code I can run directly, so I can be sure I'm testing the same thing.

You use triple backticks to enclose code blocks, for example:

    ```python
    a = abs(b)

Becomes

```python
a = abs(b)

github markdown is documented here:

https://docs.github.com/en/github/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax

jcupitt commented 2 years ago

And image-description is probably only for ASCII text, I wouldn't put unicode or even utf-8 in there.

SikangSHU commented 2 years ago

Don't post screenshots -- I can't copy-paste the code out to run it.

Post some code I can run directly, so I can be sure I'm testing the same thing.

You use triple backticks to enclose code blocks, for example:

    ```python
    a = abs(b)

Becomes

```python
a = abs(b)

github markdown is documented here:

https://docs.github.com/en/github/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax

OK, I'm so sorry for that.

SikangSHU commented 2 years ago

It should work. I see:

#!/usr/bin/python3

import sys
import pyvips

image = pyvips.Image.new_from_file(sys.argv[1])
image.set_type(pyvips.GValue.gstr_type, "image-description", "hello!")
image.write_to_file(sys.argv[2])

I can run it like this:

$ ./tiffmeta.py ~/pics/k2.jpg x.tif
$ vipsheader -a x.tif
x.tif: 1450x2048 uchar, 3 bands, srgb, tiffload
width: 1450
height: 2048
bands: 3
format: uchar
coding: none
interpretation: srgb
xoffset: 0
yoffset: 0
xres: 2.835
yres: 2.835
filename: x.tif
vips-loader: tiffload
n-pages: 1
image-description: hello!
resolution-unit: in
orientation: 1

Post a complete program I can run that shows the problem you are having.

You are right. Sorry, I have found some errors I made when installing caused me to have that question. Now I can edit the image-description. But I don't know why qupath tells me that my ome-tif file isn't a pyramidal file format after I editting the image description in this way?

final_image = pyvips.Image.tiffload('E:\\gray1_1Dsingle_inverse.tif')
final_image.set_type(pyvips.GValue.gstr_type, "image-description",
f"""
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Warning: this comment is an OME-XML metadata block, which contains crucial dimensional parameters and other important metadata. Please edit cautiously (if at all), and back up the original data before doing so. For more information, see the OME-TIFF web site: https://docs.openmicroscopy.org/latest/ome-model/ome-tiff/. -->
<OME xmlns="http://www.openmicroscopy.org/Schemas/OME/2016-06" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" Creator="OME Bio-Formats 6.7.0" UUID="urn:uuid:1053d57c-08ae-451d-9abf-1b46183ad702" xsi:schemaLocation="http://www.openmicroscopy.org/Schemas/OME/2016-06 http://www.openmicroscopy.org/Schemas/OME/2016-06/ome.xsd">
<Image ID="Image:0"><Pixels BigEndian="true" DimensionOrder="XYCZT" ID="Pixels:0" Interleaved="false" PhysicalSizeX="0.2726669676482721" PhysicalSizeXUnit="碌m" PhysicalSizeY="0.2726646815216726" PhysicalSizeYUnit="碌m" SizeC="1" SizeT="1" SizeX="66607" SizeY="53298" SizeZ="1" Type="uint16"><Channel Color="-16718848" ID="Channel:0:0" Name="Channel 1" SamplesPerPixel="1">
<LightPath/></Channel><TiffData FirstC="0" FirstT="0" FirstZ="0" IFD="0" PlaneCount="1"><UUID FileName="purple.ome.tif">urn:uuid:1053d57c-08ae-451d-9abf-1b46183ad702</UUID></TiffData></Pixels></Image><StructuredAnnotations><MapAnnotation ID="Annotation:Resolution:0" Namespace="openmicroscopy.org/PyramidResolution"><Value><M K="1">16651 13324</M><M K="2">4162 3331</M><M K="3">1040 832</M></Value></MapAnnotation></StructuredAnnotations></OME>
""")
final_image.write_to_file("E:\\single_inverse_purple.ome.tif[tile,pyramid,subifd]")  

Thank you!

jcupitt commented 2 years ago

Here's a simple converter to OME tah I've used in the ;ast:

#!/usr/bin/python3

import sys
import pyvips

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

# openslide will add an alpha ... drop it
if im.hasalpha():
    im = im[:-1]

image_height = im.height
image_bands = im.bands

# split to separate image planes and stack vertically ready for OME 
im = pyvips.Image.arrayjoin(im.bandsplit(), across=1)

# set minimal OME metadata
# before we can modify an image (set metadata in this case), we must take a 
# private copy
im = im.copy()
im.set_type(pyvips.GValue.gint_type, "page-height", image_height)
im.set_type(pyvips.GValue.gstr_type, "image-description",
f"""<?xml version="1.0" encoding="UTF-8"?>
<OME xmlns="http://www.openmicroscopy.org/Schemas/OME/2016-06"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.openmicroscopy.org/Schemas/OME/2016-06
        http://www.openmicroscopy.org/Schemas/OME/2016-06/ome.xsd">
    <Image ID="Image:0">
        <!-- Minimum required fields about image dimensions -->
        <Pixels DimensionOrder="XYCZT"
                ID="Pixels:0"
                SizeC="{image_bands}"
                SizeT="1"
                SizeX="{im.width}"
                SizeY="{image_height}"
                SizeZ="1"
                Type="uint8">
        </Pixels>
    </Image>
</OME>""")

im.tiffsave(sys.argv[2], compression="jpeg", tile=True,
            tile_width=512, tile_height=512,
            pyramid=True, subifd=True)

It expects a slide image (eg. SVS) and writes an OME TIFF. I would experiment starting from that.

SikangSHU commented 2 years ago

Thank you for your reply! I have found this in image.sc. Can I write in this way? But I found it that can't be identified as a pyramidal format file.

import pyvips

im = pyvips.Image.new_from_file('E:\\gray1_1Dzuikaishi.tif')

# openslide will add an alpha ... drop it
if im.hasalpha():
    im = im[:-1]

image_height = im.height
image_bands = im.bands

# split to separate image planes and stack vertically ready for OME
im = pyvips.Image.arrayjoin(im.bandsplit(), across=1)

# set minimal OME metadata
# before we can modify an image (set metadata in this case), we must take a
# private copy
im = im.copy()
im.set_type(pyvips.GValue.gint_type, "page-height", image_height)
im.set_type(pyvips.GValue.gstr_type, "image-description",
f"""<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<OME xmlns="http://www.openmicroscopy.org/Schemas/OME/2016-06"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    Creator="OME Bio-Formats 6.7.0" UUID="urn:uuid:1053d57c-08ae-451d-9abf-1b46183ad702"
    xsi:schemaLocation="http://www.openmicroscopy.org/Schemas/OME/2016-06
        http://www.openmicroscopy.org/Schemas/OME/2016-06/ome.xsd">
    <Image ID="Image:0">
        <!-- Minimum required fields about image dimensions -->
        <Pixels BigEndian="true" DimensionOrder="XYCZT"
                ID="Pixels:0"
                Interleaved="false"
                SizeC="{image_bands}"
                SizeT="1"
                SizeX="{im.width}"
                SizeY="{image_height}"
                SizeZ="1"
                Type="uint8">
        <Channel Color="-16718848" ID="Channel:0:0" Name="Channel 1" SamplesPerPixel="1">
        <LightPath/></Channel><TiffData FirstC="0" FirstT="0" FirstZ="0" IFD="0" PlaneCount="1">
        </Pixels>
    </Image>
</OME>""")

im.tiffsave('E:\\single_inverse_purple.ome.tif', compression="jpeg", tile=True,
            tile_width=512, tile_height=512,
            pyramid=True, subifd=True)
(learn) C:\Users\91765>vipsheader -a E:\\single_inverse_purple.ome.tif
E:\\single_inverse_purple.ome.tif: 66607x53298 uchar, 1 band, b-w, tiffload
width: 66607
height: 53298
bands: 1
format: uchar
coding: none
interpretation: b-w
xoffset: 0
yoffset: 0
xres: 3667.5
yres: 3667.56
filename: E:\\single_inverse_purple.ome.tif
vips-loader: tiffload
n-subifds: 8
n-pages: 1
image-description: <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<OME xmlns="http://www.openmicroscopy.org/Schemas/OME/2016-06"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    Creator="OME Bio-Formats 6.7.0" UUID="urn:uuid:1053d57c-08ae-451d-9abf-1...
resolution-unit: cm
orientation: 1
SikangSHU commented 2 years ago

I found it that the program works well and can be identified as a pyramidal format file successfully if the input is <66607x53298 uchar, 1 band, b-w, tiffload>. Maybe my input is <66607x53298 ushort, 1 band, grey16, tiffload>, so the error happens.

jcupitt commented 2 years ago

Oh, maybe QuPath can't do 16-bit images?

I would make a small test image and post it on image.sc. Perhaps the qupath devs can help.

SikangSHU commented 2 years ago

Oh, maybe QuPath can't do 16-bit images?

I would make a small test image and post it on image.sc. Perhaps the qupath devs can help.

Hi, Qupath can do 16-bit images. I found that the program works well and can be identified as a pyramidal format file by Qupath successfully if I add </TiffData> in the image-description. My code is as follows. Thank you for your help!

import pyvips

final_image = pyvips.Image.tiffload('E:\\gray1_1Dsingle_inverse.tif')
final_image.set_type(pyvips.GValue.gstr_type, "image-description",
f"""
<OME xmlns="http://www.openmicroscopy.org/Schemas/OME/2016-06"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    Creator="OME Bio-Formats 6.7.0"
    xsi:schemaLocation="http://www.openmicroscopy.org/Schemas/OME/2016-06
        http://www.openmicroscopy.org/Schemas/OME/2016-06/ome.xsd">
    <Image ID="Image:0">
        <!-- Minimum required fields about image dimensions -->
        <Pixels DimensionOrder="XYCZT"
                ID="Pixels:0"
                SizeC="1"
                SizeT="1"
                SizeX="66607"
                SizeY="53298"
                SizeZ="1"
                Type="uint16">
        <Channel Color="-16718848" ID="Channel:0:0" Name="Channel 1" SamplesPerPixel="1"><LightPath/></Channel><TiffData FirstC="0" FirstT="0" FirstZ="0" IFD="0" PlaneCount="1">
        </TiffData>
        </Pixels>
    </Image>
</OME>
""")
final_image.write_to_file("E:\\single_inverse_purple.ome.tif[tile,pyramid,subifd]")
jcupitt commented 2 years ago

Oh good. I would fix the indenting, ie.:

        <Pixels DimensionOrder="XYCZT"
                ID="Pixels:0"
                SizeC="{final_image.bands}"
                SizeT="1"
                SizeX="{final_image.width}"
                SizeY="{final_image.height}"
                SizeZ="1"
                Type="uint16">
          <Channel Color="-16718848" ID="Channel:0:0" Name="Channel 1" SamplesPerPixel="1">
            <LightPath/>
          </Channel>
          <TiffData FirstC="0" FirstT="0" FirstZ="0" IFD="0" PlaneCount="1">
          </TiffData>
        </Pixels>
SikangSHU commented 2 years ago

Hello! I need to input the number of Colour from the command line. How can I use sys.argv and not change the format of the file? When I write the program as follows, Colour can't be editted.

gray1_1D = gray1[0]
filename = f"CD8.tif[tile,pyramid,subifd]"
print(f"writing {filename} ...")
col1 = int(sys.argv[1])
print(col1)
CD8 = (255 - gray1_1D * 255).cast("int").colourspace("grey16")
CD8.set_type(pyvips.GValue.gstr_type, "image-description",
f"""
<OME xmlns="http://www.openmicroscopy.org/Schemas/OME/2016-06"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    Creator="OME Bio-Formats 6.7.0"
    xsi:schemaLocation="http://www.openmicroscopy.org/Schemas/OME/2016-06
        http://www.openmicroscopy.org/Schemas/OME/2016-06/ome.xsd">
    <Image ID="Image:0">
        <!-- Minimum required fields about image dimensions -->
        <Pixels DimensionOrder="XYCZT"
                ID="Pixels:0"
                Interleaved="false"
                SizeC="1"
                SizeT="1"
                SizeX="66607"
                SizeY="53298"
                SizeZ="1"
                Type="uint16">
        <Channel Color=col1 ID="Channel:0:0" Name="Channel 1" SamplesPerPixel="1"><LightPath/></Channel><TiffData FirstC="0" FirstT="0" FirstZ="0" IFD="0" PlaneCount="1">
        </TiffData>
        </Pixels>
    </Image>
</OME>
""")
CD8.tiffsave('E:\\CD8.tif', tile=True, pyramid=True, subifd=True)
(learn) C:\Users>python E:/Learning/3c_pyvips.py -16718848