Peter554 / StainTools

Tools for tissue image stain normalisation and augmentation in Python 3
MIT License
321 stars 107 forks source link

Floating point exception (core dumped) #26

Closed EKami closed 5 years ago

EKami commented 6 years ago

Hello @Peter554, I get a Floating point exception (core dumped) when I'm using transform(img) from staintools.StainNormalizer(method='vahadane') with stain_normalizer.fit(i1_standard) previously applied and i1_standard being the same image as given in the example. Here is a sample code to reproduce the error:

import staintools
import numpy as np

i1 = staintools.read_image("i1.png")
img = np.load("out.npy")

stain_standardizer = staintools.BrightnessStandardizer()
stain_normalizer = staintools.StainNormalizer(method='vahadane')

i1_standard = stain_standardizer.transform(i1)
stain_normalizer.fit(i1_standard)

print("img shape", img.shape)
print("img dtype", img.dtype)
img = stain_standardizer.transform(img)
img = stain_normalizer.transform(img)

Output:

img shape (128, 128, 3)
img dtype uint8
Floating point exception (core dumped)

Any idea why this happens? I've put the npy array file here. Thanks a lot for this amazing tool!

Peter554 commented 6 years ago

Can I ask which version of Python you are using? Also can you also clarify exactly which line of the above is causing the error? Thanks

EKami commented 6 years ago

@Peter554 Sure, Python 3.6.6, the line causing the error is the last one:

img = stain_normalizer.transform(img)

I installed spams with conda

EKami commented 6 years ago

I also obtain:

RuntimeWarning: divide by zero encountered in true_divide
  source_concentrations *= (self.maxC_target / maxC_source)

In some of my images

EKami commented 6 years ago

If I use staintools.StainNormalizer(method='macenko') instead I get:

numpy.linalg.linalg.LinAlgError: Eigenvalues did not converge

If that can help. Getting an exception by using the StainNormalizer is kinda ok, at least I can catch it and continue the execution of my code. But if I get a Floating point exception like above there is nothing much I can do.

Peter554 commented 6 years ago

It seems you are getting zero for the max concentration on img. I believe you load this from a numpy array img = np.load("out.npy"). Can you please send a picture of this image? Is it in the correct format (uint8: unsigned integers in range 0-255)?

EKami commented 6 years ago

@Peter554 it's mostly a white image (RGB range 0-255 with dtype uint8). I understand this is not the kind of images that's expected to fit to the normalizer but my point is that I shouldn't get a segfault if I use a valid image while this image doesn't have the right concentration. I should, at worse, get a python exception that I can catch and let my program continue.

The way I planned to use your tool is to stick it to my deep learning dataset pipeline so all kind of images can go through it, I eventually could check the image content to make sure I do not pass images that the normalizer do not expect but in that case I would have to rely on my own assumptions to create the condition which prevent my whole program from crashing.

Instead your lib could just ignore the stain normalization on a given image (which is what makes the most sense to me) if that same image is not what your lib expect or at least throw a python exception then I wouldn't have to create hacky conditions to avoid crashes.

The sample code I posted in that thread along with the saved numpy array are sufficient for you to be able to reproduce the crash. I would like to be able to submit a PR but I'm not sure I would make this right. Thanks :)

Peter554 commented 6 years ago

Yes I agree the current behaviour is poor. If you have a solution that amends this behaviour a PR would certainly be welcome.

EKami commented 6 years ago

Hey @Peter554 I just checked and it seems the segfault is actually happening in SPAMS. spams.trainDL() in VahadaneStainExtractor seems to be the culprit (the segfault happens right after the get_stain_matrix returns so that's very weird as everything seems to happen smoothly then fails). You can reproduce by adding this test to your test suite:

import os
from pathlib import Path
import staintools
from staintools.utils.misc_utils import *
import numpy as np

def test_stain_normalizer():
    # Ensure the test do not segfault
    x = np.array([[255, 255, 250],
                  [255, 255, 255],
                  [255, 255, 255]], dtype=np.uint8)
    x = np.repeat(x[:, :, np.newaxis], 3, axis=2)  # Adds 3 channels
    stain_normalizer = staintools.StainNormalizer(method='vahadane')
    script_dir = Path(os.path.dirname(os.path.abspath(__file__)))
    i1 = staintools.read_image(str(script_dir / ".." / "data" / "i1.png"))
    stain_normalizer.fit(i1)

    output = stain_normalizer.transform(x)
    assert output.shape == x.shape

I really don't have any idea how to deal with SPAMS, do you have any recommendation of a precheck on my images that we talked about that I could add which will prevent me to end up with a segfault? Thanks a lot!

gdurif commented 5 years ago

Hi, I am part of the SPAMS dev team. I found the issue. In both examples, resp.:

img = np.load("out.npy")
stain_normalizer = staintools.StainNormalizer(method='vahadane')
img = stain_normalizer.transform(img)

and

x = np.array([[255, 255, 250],
                     [255, 255, 255],
                     [255, 255, 255]], dtype=np.uint8)
x = np.repeat(x[:, :, np.newaxis], 3, axis=2)  # Adds 3 channels
stain_normalizer = staintools.StainNormalizer(method='vahadane')
output = stain_normalizer.transform(x)

both images (img and x) are fully masked in the method VahadaneStainExtractor.get_stain_matrix (see staintools/stain_extractors/vahadane_stain_extractor.py), thus the decomposition from SPAMS is applied to an empty array, hence the segfault.

Here is a reproducible example:

import numpy as np
x = np.array([[255, 255, 250],
                     [255, 255, 255],
                     [255, 255, 255]], dtype=np.uint8)
x = np.repeat(x[:, :, np.newaxis], 3, axis=2)  # Adds 3 channels

## see staintools/stain_extractors/vahadane_stain_extractor.py
from staintools.utils.misc_utils import convert_RGB_to_OD, normalize_rows, get_luminosity_mask
luminosity_threshold=0.8
dictionary_regularizer=0.1
I=x
tissue_mask = get_luminosity_mask(I, threshold=luminosity_threshold).reshape((-1,))
OD = convert_RGB_to_OD(I).reshape((-1, 3))
OD = OD[tissue_mask]
print(OD)
print(OD.shape)

import spams
dictionary = spams.trainDL(X=OD.T, K=2, lambda1=dictionary_regularizer, mode=2, modeD=0, posAlpha=True, posD=True, verbose=False).T # segfault: OD.T is an empty array

I will see how we can add som checks in SPAMS for such scenario. In the mean time it might probably be a quickier fix if there was a check before using spams in StainTools.

EKami commented 5 years ago

Hello, Thank you so much for taking the time to investigate on this issue! As I said on the github issue a python programmer don't expect to get a segfault when something goes wrong, he must, at best, get a python exception.

I will prob be able to submit a quick fix to the staintools repo thanks to your help but SPAMS would become more robust if it is bulletproof to these kind of failures :).

Again, thanks a lot for the help.

Passe une bonne journée.

On Tue, Nov 13th, 2018 at 1:8 PM, gdurif notifications@github.com wrote:

Hi, I am part of the SPAMS dev team. I found the issue. In both examples, resp.:

img = np.load("out.npy") stain_normalizer = staintools.StainNormalizer(method='vahadane') img = stain_normalizer.transform(img)

and

x = np.array([[255, 255, 250], [255, 255, 255], [255, 255, 255]], dtype=np.uint8) x = np.repeat(x[:, :, np.newaxis], 3, axis=2) # Adds 3 channels stain_normalizer = staintools.StainNormalizer(method='vahadane') output = stain_normalizer.transform(x)

both images ( img and x ) are fully masked in the method VahadaneStainExtractor.get_stain_matrix (see staintools/stain_extractors/vahadane_stain_extractor.py ), thus the decomposition from SPAMS is applied to an empty array, hence the segfault.

Here is a reproducible example:

import numpy as np x = np.array([[255, 255, 250], [255, 255, 255], [255, 255, 255]], dtype=np.uint8) x = np.repeat(x[:, :, np.newaxis], 3, axis=2)

Adds 3 channels ## see

staintools/stain_extractors/vahadane_stain_extractor.py from staintools.utils.misc_utils import convert_RGB_to_OD, normalize_rows, get_luminosity_mask luminosity_threshold=0.8 dictionary_regularizer=0.1 I=x tissue_mask = get_luminosity_mask(I, threshold=luminosity_threshold).reshape((-1,)) OD = convert_RGB_to_OD(I).reshape((-1, 3)) OD = OD[tissue_mask] print(OD) print(OD.shape) import spams dictionary = spams.trainDL(X=OD.T, K=2, lambda1=dictionary_regularizer, mode=2, modeD=0, posAlpha=True, posD=True, verbose=False).T # segfault: OD.T is an empty array

I will see how we can add check in SPAMS for such scenario. In the mean time it might probably be a quickier fix if there was a check before using spams in StainTools.

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub ( https://github.com/Peter554/StainTools/issues/26#issuecomment-438243832 ) , or mute the thread ( https://github.com/notifications/unsubscribe-auth/AD2AopzMhL94ve5sHZ4XGWdhpX3Z-NB3ks5uurZEgaJpZM4X10ZJ ).

gdurif commented 5 years ago

Thanks for the tip. I will try to look into it as soon as possible.

EKami commented 5 years ago

PR submitted here: https://github.com/Peter554/StainTools/pull/27 . Ignoring the stain normalization in the entire image may be a better solution than raising an exception, your call @Peter554

Peter554 commented 5 years ago

Should be fixed now. Thanks.