I'm trying to do a histogram equalization on a 16-bit gray scale image, the original histogram is shown below, which has ~25000 gray levels:
I was first using MATLAB but for some reason the total levels was significantly reduced in the output (only 21!). I tried to manually assign a bin number of 20,000 but the output level is still minimal (67).
I then tried Scikit-Image in Python and everything works as expected -- output now has 16,500 levels and the histogram is pretty much flat.
Here's the MATLAB command:
J = histeq(I,2e4);
Here's Python command:
img_eq_sk = exposure.equalize_hist(img_16bit)
Since histogram equalization is such a basic operation, I would expect MATLAB and Python behave similarly, but according to this MATLAB's result isn't even nearly as good as Skimage's.
I can verify what you're seeing:
I = uint16(randn(1000,1000) * 5000 + 3e4);
imhist(I)
size(unique(I(:))) % returns: 31290 unique gray values
J = histeq(I,8e5); % actually uses only 6.5e4, the max for uint16
imhist(J)
size(unique(J(:))) % returns: 158 unique gray values
K = histeq(im2double(I),8e5);
imhist(K)
size(unique(K(:))) % returns: 175 unique gray values
When the input values need to be stretched significantly, many different gray values end up in the same output bin, and many output bins remain empty.
When looking at the result with a double image (K in the code above) one can see how the number of unique output gray levels increases as one increases the parameter to histeq.
That is, the output is quantized to a set number of values (800,000 in the example above), not only the input. So if many of those output bins are empty, there will be very few distinct output gray values.
There is no need to implement histogram equalization this way, as can be seen by the Python implementation used in the OP. However, it doesn't seem that this implementation is wrong, it just quantizes the output unnecessarily.
<opinion> However, since histogram equalization is something useful only for visualization, and since for visualization one doesn't need more than about 100 distinct gray values (we can't distinguish more than that anyway), the output quantization should not be a major problem. If you're using histogram equalization for anything else (i.e. image analysis and quantification) you're doing it wrong! </opinion>
Related
I'm trying to filter out images that do not contain any or much visible structure from those that have a visible object in them so I can feed them into an self-supervised neural network.
I want to keep images like this, and I want to remove images like this:
I'm converting chemical imaging data to numpy arrays containing the signal intensity as float data, then use matplotlib to generate these images. To try to filter out the blank images, I first smoothed the images by setting each pixel value to the mean of its surrounding pixel to minimize noise. Then I found the standard deviation (σ) and mean (μ) and tried to filter out the bad images based on a σ, σ/μ, or σ^2/μ threshold, the latter of which somewhat worked. But if I set a threshold each image must exceed, such as σ^2/μ = 500, to apply to all datasets, it would remove far too many images from some or remove few to none from others.
Here's an example of me smoothing out the image and comparing σ^2/μ.
np.load(example.npy)
smoothed_image = np.empty(imgs.shape[1:])
for i, image in enumerate(imgs):
for x in range(imgs.shape[1]):
for y in range(imgs.shape[2]):
# Select pixels to average
subset = image[np.clip(x-3, 0, None):np.clip(x+4, None, image.shape[0]-1),
np.clip(y-3, 0, None):np.clip(y+4, None, image.shape[1]-1)]
subset_ave = np.mean(subset)
smoothed_image[x,y] = subset_ave
smoothed_image[x,y]
# Show stddev^2/mean and related image
print(f'stddev^2/mean = {smoothed_image.std()**2/smoothed_image.mean()})
plt.imshow(image)
plt.show()
plt.close()
I need to filter this data in an unsupervised fashion, so checking and changing the threshold for each dataset isn't an option. In addition, this process adds a significant amount of time to my data processing due to the ordering of my workflow. I tried to find other options online, but I don't think I know what to search to find information about this specific issue.
Here is some example data. Selecting any index on axis 0 (ex. images[8]) will give you a single image array.
Any suggestions on what methods I could use to filter images like this, preferably without very time consuming computation?
Thanks in advance!
My first thought is to use an aggressive threshold to suppress nearly all the noise, then simply take the sum of the image and set a threshold that way, kind of like:
image_thresh = image - 100 # where image is a numpy array and 100 would surely suppress noise, but not features
image_thresh[image_thresh<0] = 0
image_sum = np.sum( image_thresh )
Another way is to use OpenCV and look for ellipses above a certain size. You could reference such a page as this one to get started on that.
I'm trying to port some code that was originally written in scikit to OpenCV, as I already use OpenCV for some other tasks. I have these two images:
which are essentially the polar forms of two images that share a common center, where one image is a rotation of the other. I need to use phase correlation to determine what this angle is. In OpenCV, I'm doing:
import cv2
import numpy as np
im1 = np.float32(cv2.cvtColor(cv2.imread('polar-part.png'), cv2.COLOR_BGR2GRAY))
im2 = np.float32(cv2.cvtColor(cv2.imread('polar-template.png'), cv2.COLOR_BGR2GRAY))
print(cv2.phaseCorrelate(im1, im2))
Which produces the incorrect answer
((-0.07302320870314816, -0.19596856380076133), 0.03418491033860195)
In Scikit, I do
template_polar = rgb2gray(imread('polar-template.png'))
up_cam_polar = rgb2gray(imread('polar-part.png'))
print(phase_cross_correlation(up_cam_polar, template_polar, upsample_factor=20))
which produces the correct answer of
(array([ 1.3625e+02, -5.0000e-02]), 0.2080647049014251, 2.6434620698588315e-07)
The import number here is the y shift, which is about 136. This is the correct number of pixels to translate one image onto the other.
Why does OpenCV give back a drastically different answer?
Best I can tell is as follows from the documentation.
Skimage is returning the correct offsets for your images:
Returns the (y,x) shifts and the normalized rms error.
OpenCV is returning incorrect offsets for your images (phase correlation can be very sensitive to noise and non-cyclic images). Your only available argument would be to adjust the windowing:
Returns the (x,y) shifts and the sum of the elements of the correlation (r) within the 5x5 centroid around the peak location. It is normalized to a maximum of 1
Here is a different example:
Input 1:
Input 2:
Results:
opencv: ((20.20846249610655, 22.459076722413144), 0.5959940360504904)
skimage: (array([-22., -20.]), 0.31940809429051836, -2.0134398e-10)
which shows equivalent shifts. (The signs are opposite depending upon how the two methods assign the reference and target images). The correlation metric values returned are different, due to the different ways that they are computed and normalized.
The type of my train_data is 'Array of unit 16'. The size is (96108,7,7). Therefore, there are 96108 images.
The image is different from the general image. My image has a sensor of 7x7 and 49 pixels contain the number of detected lights. And one image is the number of light detected for 0 to 1 second. Since the sensor detects randomly for a unit time, the maximum values of the pixel are all different.
If the max value of all images is 255, I can do 'train data/255', but I can't use the division because the max value of the image I have is all different.
I want to make the pixel value of all images 0 to 1.
What should I do?
Contrast Normalization (or contrast stretch) should not be confused with Data Normalization which maps data between 0.0-1.0.
Data Normalization
We use the following formula to normalize data. The min() and max() values are the possible minimum and maximum values supported within the type of data.
When we use it with images, x is the whole image and i is an individual pixel of that image. If you are using an 8-bit image the min() and max() values become 0 and 255 respectively. This should not be confused with the minimum and maximum values presented within your image in question.
To convert an 8-bit image into a floating-point image, As min() value reaches 0, the simple math is image/255.
img = img/255
NumPy methods likes to output arrays in 64-bit floating-point by default. To effectively test methods applied to 8-bit images with NumPy, an 8-bit array is required as the input:
image = np.random.randint(0,255, (7,7), dtype=np.uint8)
normalized_image = image/255
When we examine the output of the above two lines we can see the maximum value of the image is 252 which has now mapped to 0.9882352941176471 on the 64-bit normalized image.
However, in most cases, you wouldn't need a 64-bit image. You can output (or in other words cast) it to 32-bit (or 16-bit) using the following code. If you try to cast it to 8-bit it will throw an error. Using '/' for division is a shorthand for np.true_divide but lacks the ability to define the output data format.
normalized_image_2 = np.true_divide(image, 255, dtype=np.float32)
The properties of the new array is shown below. You can see the number of digits are now reduced and 252 has been remapped to 0.9882353.
Contrast Normalization
The method shown by #3dSpatialUser effectively does a partial contrast normalization, meaning it stretches the intensities of the image within the available intensity range. Test it with an 8-bit array with the following code.
c_image = np.random.randint(64,128, (7,7), dtype=np.uint8)
cn_image = (c_image - c_image.min()) / (c_image.max()- c_image.min())
Contrast is now stretched mapping minimum contrast of 64 to 0.0 and maximum 127 to 1.0.
The formula for contrast normalization is shown below.
Using the above formula with NumPy and to remap data back to the 8-bit input format after contrast normalization, the image should be multiplied by 255, then change the data type back to unit8:
cn_image_correct = (c_image - c_image.min()) / (c_image.max()- c_image.min()) * 255
cn_image_correct = cn_image_correct.astype(np.int8)
64 is now mapped to 0 and 174 is mapped to 255 stretching the contrast.
Where the confusion arise
In most applications, the intensity values of an image are spread close to their minima and maxima. Hence, when we apply the normalization formula using the min and max values presented within the image, instead of the min max of the available range, it will output a better looking image (in most cases) within the 0.0-1.0 range, which effectively does normalize both data and contrast at the same time. Also, image editing software perform gamma corrections or remapping when switching between image data types 8/16/32-bits.
import numpy as np
data = np.random.normal(loc=0, scale=1, size=(96108, 7, 7))
data_min = np.min(data, axis=(1,2), keepdims=True)
data_max = np.max(data, axis=(1,2), keepdims=True)
scaled_data = (data - data_min) / (data_max - data_min)
EDIT: I have voted for the other answer since that is a cleaner way (in my opinion) to do it, but the principles are the same.
EDIT v2: I saw the comment and I see the difference. I will rewrite my code so it is "cleaner" with less extra variables but still correct using min/max:
data -= data.min(axis=(1,2), keepdims=True)
data /= data.max(axis=(1,2), keepdims=True)
First the minimum value is moved to zero, thereafter one can take the maximum value to get the full range (max-min) of the specific image.
After this step np.array_equal(data, scaled_data) = True.
You can gather the maximum values with np.ndarray.max across multiple axes: here axis=1 and axis=2 (i.e. on each image individually). Then normalize the initial array with it. To avoid having to broadcast this array of maxima yourself, you can use the keepdims option:
>>> x = np.random.rand(96108,7,7)
>>> x.max(axis=(1,2), keepdims=True).shape
(96108, 1, 1)
While x.max(axis=(1,2)) alone would have returned an array shaped (96108,)...
Such that you can then do:
>>> x /= x.max(axis=(1,2), keepdims=True)
I have image that contains many no data pixels. The image is 2d numpy array and the no-data values are "None". Whenever I try to apply on it filters, seems like the none values are taken into account into the kernel and makes my pixels dissapear.
For example, I have this image:
I have tried to apply on it the lee filter with this function (taken from Speckle ( Lee Filter) in Python):
from scipy.ndimage.filters import uniform_filter
from scipy.ndimage.measurements import variance
def lee_filter(img, size):
img_mean = uniform_filter(img, (size, size))
img_sqr_mean = uniform_filter(img**2, (size, size))
img_variance = img_sqr_mean - img_mean**2
overall_variance = variance(img)
img_weights = img_variance / (img_variance + overall_variance)
img_output = img_mean + img_weights * (img - img_mean)
return img_output
but the results looks like this:
with the warnning:
UserWarning: Warning: converting a masked element to nan. dv =
np.float64(self.norm.vmax) - np.float64(self.norm.vmin)
I have also tried to use the library findpeaks.
from findpeaks import findpeaks
import findpeaks
#lee enhanced filter
image_lee_enhanced = findpeaks.lee_enhanced_filter(img, win_size=3, cu=0.25)
but I get the same blank image.
When I used median filter on the same image with ndimage is worked no problem.
My question is how can I run those filters on the image without letting the None values interrupt the results?
edit: I prefer not to set no value pixels to 0 because the pixel range value is between -50-1 (is an index values). In addition i'm afraid that if I change it to any other value e.g 9999) it will also influence the filter (am I wrong?)
Edit 2:
I have read Cris Luengo answer and I have tried to apply something similar with the scipy.ndimage median filter as I have realized that the result is disorted as well.
This is the original image:
I have tried masking the Null values:
idx = np.ma.masked_where(img,img!=None)[:,1]
median_filter_img = ndimage.median_filter(img[idx].reshape(491, 473), size=10)
zeros = np.zeros([img.shape[0],img.shape[1]])
zeros[idx] = median_filter_img
The results looks like this (color is darker to see the problem in the edges):
As it can bee seen, seems like the edges values are inflluences by the None values.
I have done this also with img!=0 but got the same problem.
(just to add: the pixels vlues are between 1 to -35)
If you want to apply a linear smoothing filter, then you can use the Normalized Convolution.
The basic recipe is:
Create a mask image that is 1 for the pixels with data, and 0 for the pixels without data.
Set the pixels without data to any number, for example 0. NaN is not valid because it spreads in the computations.
Apply the linear smoothing filter to the image multiplied by the mask.
Apply the linear smoothing filter to the mask.
Divide the two results.
Basically, we normalize the result of the linear smoothing filter (convolution) by the number of pixels with data within the filter window.
In regions where the smoothed mask is 0 (far away from data), we will divide 0 by 0, so special care needs to be taken there.
Note that normalized convolution can be used also for uncertain data, where the mask image gets values in between 0 and 1 indicating the confidence we have in each pixel. Pixels thought to be noisy can be set to a value closer to 0 than the other pixels, for example.
The recipe above is only valid for linear smoothing filters. Normalized convolution can be done with other linear filters, for example derivative filters, but the resulting recipe is different. See for example here the equation for Normalized Convolution to compute the derivative.
For non-linear filters, other approaches are necessary. Non-linear smoothing filters, for example, will often avoid affecting edges, and so will work quite well in images with missing data, if the missing pixels are set to 0, or some value far outside of the data range. The concept of keeping a mask image that indicates which pixels have data and which don't is always a good idea.
Seems like a simple solution is to set the non values to zero. I don't know how you would get around this, because most image processing kernels require some value to for you to apply.
a[numpy.argwhere(a==None)] = 0
I've looked around and haven't found a straightforward solution to this seemingly simple problem online or in the matplotlib documentation. I'm using imshow() to create a heatmap of a matrix with a diverging cmap (coolwarm), and I'd like to make 0 represented as white, positive values as red, and negative values as blue. Anyone know of an easy way to do this without creating a custom cmap?
By using min-max normalization, here zero in the original data gets shifted, so shift the zero as shown below. The below is a way I figured that can be done in this way.
data = np.array([[0.000000,5.67],[-0.231049,0.45],[-0.231049,0.000000]])
k=(data-np.min(data))/(np.max(data)-np.min(data)) # Min-max Normalization
nsv_zero =-np.min(data)/(np.max(data)-np.min(data)) # new shifted value of zero
sns.heatmap(np.where( k == nsv_zero ,0.5 ,k),vmin=0,vmax=1,cmap='coolwarm',annot=data)
* Min-Max Normalization : Scale all the values to new range(0,1)
* Shifting zero in initial data to 0.5 in the new output data so as to get white color
* Here I am using modified data on heatmap, but I am using the original annotations only.
Hope this is a bit closer to your required output