Image -> Type -> 8 bit have unexpected behavior
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.

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

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
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)
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.
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…
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:
- 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.
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.)