Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Image -> Type -> 8 bit have unexpected behavior #134

Open
Michael-H777 opened this issue Oct 8, 2021 · 5 comments
Open

Image -> Type -> 8 bit have unexpected behavior #134

Michael-H777 opened this issue Oct 8, 2021 · 5 comments

Comments

@Michael-H777
Copy link

Michael-H777 commented Oct 8, 2021

Hello all,

I am working with imageJ and python. My data is float32, and was converted into int8 in ImageJ. After investigating, I realized that binning into 0 -> 255 range using ImageJ (Image- > type -> int8) have different distribution vs binning into 0 -> 255 range using python. Code to reproduce as follow

import numpy as np 
import tifffile 

np.random.seed(20) # seed is only for reproducing min/max, described behavior continues to show without using seed 
test_data = np.random.normal(size=(5, 500, 500))
test_data = test_data.cumsum(axis=0).astype(np.float32)

# at this stage, test_data[0] have smaller range than test_data itself due to cumsum() 
print(test_data.min(), test_data.max()) #prints: -10.676156, 10.48997
print(test_data[0].min(), test_data[0].max()) #prints: -4.385402 4.4191904

tifffile.imwrite('test.tif', test_data)

# manually convert float32 data into int8 using imageJ

# please note that converting test_data into min_max_location then multiply by 255, then round should in theory yield 
# equivalent result to converting float32 into int8 using a linear scaling
min_max_location = (test_data - test_data.min()) / (test_data.max() - test_data.min())
python_binning = (min_max_location * 255).round() # this operation bins the test_data into 0->255 range using a lienar scaling 
imagej_binned = tifffile.imread('test-1.tif') # read the data binned by imageJ 

However, after plotting the data, the distribution changed dramatically.

please note the difference in y-axis scale between python-binned data vs imagej-binned data
as well as difference in number of 0 and 255 between python-binned data vs imagej-binned data
Also pay attention to how the shape of the distribution changed, original data and python-binned data both have taller and skinner distribution compared to imageJ binned data.

image

After further investigation, the binning is done using the display range in ImageJ -> Image -> Show Info that only captures the range of first slice of stack

image

and not the -10.676156, 10.48997 range of the entire stack printed by python.

Shouldn't the int8 conversion min/max reflect the entire stack's distribution? Please let me know.

@rasband

python version: 3.8.11
numpy version: 1.20.3
ImageJ version: ImageJ 1.53k; Java 1.8.0_172 [64-bit]
python platform: Linux version 5.11.0-37-generic (buildd@lcy01-amd64-021) (gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0, GNU ld (GNU Binutils for Ubuntu) 2.34)
ImageJ platform: win10

@aschain
Copy link

aschain commented Oct 12, 2021

I think the type-> 8bit command converts the image based on whatever the current min/max is, not just the first slice per se. This allows the ability to arbitrarily set the min/max for specific conversions.

Perhaps it would be good for ImageJ to set the min/max according to the whole stack when opening for the first time, rather than just the first displayed slice?

However, you might be able to avoid this problem by setting the min/max in the python-generated tif file, something like:

tifffile.imwrite('test.tif', test_data, imagej=True, metadata={'min': test_data.min(), 'max': test_data.max()})

That way ImageJ would open with the specified min and max? (I did not test this, FYI)

@Michael-H777
Copy link
Author

Michael-H777 commented Oct 12, 2021

tifffile.imwrite('test.tif', test_data, imagej=True, metadata={'min': test_data.min(), 'max': test_data.max()})

Thanks for the reply, indeed this solves the issue. Now all the slices in ImageJ have the specified min and max.

However, the original intent of this issue was to convey the problem that 1) ImageJ implicitly changes the data distribution without alerting the user and 2) that type conversion should not have alter data's 'relative location' with respect to the entire dataset unless otherwise specified. Shouldn't there at least be options to use min/max from current slice, min/max from entire stack, or specified min/max?

For those that does not know python, are there any fix to this problem? Converting data type should not alter the original distribution.

For context: our downstream task is image segmentation, hence changes in distribution will affect the final segmentation result. Besides, the current ImageJ type conversion behavior truncates a lot of data.

@imagejan
Copy link
Member

From the user guide:

8-bit Converts to 8-bit grayscale. ImageJ converts 16-bit and 32-bit images to 8-bit by linearly scaling from min--max to 0--255, where min and max are the two values displayed in the Image▷Adjust▷Brightness/Contrast… [C]↓. Image▷Show Info… [i]↓ displays these two values as Display range. Note that this scaling is not done if Scale When Converting is not checked in Edit▷Options▷Conversions…

@Michael-H777
Copy link
Author

Michael-H777 commented Oct 18, 2021

From the user guide:

8-bit Converts to 8-bit grayscale. ImageJ converts 16-bit and 32-bit images to 8-bit by linearly scaling from min--max to 0--255, where min and max are the two values displayed in the Image▷Adjust▷Brightness/Contrast… [C]↓. Image▷Show Info… [i]↓ displays these two values as Display range. Note that this scaling is not done if Scale When Converting is not checked in Edit▷Options▷Conversions…

I'm not sure what suggestion/recommendation/solution are you proposing to the problem I was discussing:

  1. ImageJ implicitly changes the data distribution without alerting the user and 2) that type conversion should not have alter data's 'relative location' with respect to the entire dataset unless otherwise specified.

I already stated that I understand what cause this behavior, but believe it is misleading and incorrect.

@imagejan
Copy link
Member

imagejan commented Oct 18, 2021

Sorry for brevity, I just wanted to put the issue in context, indicating that this behavior is "by design" and unlikely to change, since that would break any existing workflows with ImageJ 1.x, even though I agree that the current behavior, and in particular the inconsistency between '8-bit' and the other options (16-bit, 32-bit), may be counter-intuitive.

(Note that ImageJ Ops - part of ImageJ2 and therefore Fiji - provide more control over the conversion behavior, by clearly separating normalization from scaling when converting between types. That's however accessible only via scriptint, and not via the legacy UI.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants