Brightness equalization using PIL in Python - python-3.x

There are multiple grayscale images in a folder that I am trying to equalize the brightness. The enhancement has to be applied in all the images except the first which will be our reference image. Also, the change only happens in the brightness of the images and not contrast since I want to preserve all the other details. This means that there is only a simple shift of histogram and no widening. I was trying to use the PIL module to calculate the average brightness. This method is probably faster since I can do it without a numpy array conversion.
The code:
with open("Img_Locations.txt") as file: # Assume the file locations in a text file
img_files = file.read().splitlines()
file.close()
self.images = [[] for x in range(len(img_files))]
for i in range(len(img_files)):
images[i] = Image.open(img_files[i])
im = images[i].convert('L')
stat = ImageStat.Stat(im)
ref_pil_original = int(stat.mean[0])
enhancer = ImageEnhance.Brightness(im)
enhanced_im = enhancer.enhance(1.5)
stat2 = ImageStat.Stat(enhanced_im)
ref_pil_bright = int(stat2.mean[0])
print(ref_pil_original, ref_pil_bright)
The sample output here was:
114 170
129 192
122 183
So the question is why does brightness enhancement when applied to the same factor of 1.5 yield me different differences for each image (basically how does the factor affect the pixels of my image)? If this is the case, what is the fastest way that I can adjust the brightness of my images by a constant factor to make sure that the final averages have the same mean value?
python -- measuring pixel brightness Here the PIL image is read by pixel. So does this mean that I will have to scan the image pixel by pixel to complete my requirement? I would like to know if this is indeed the way to do this since there are already existing functions. Unfortunately, I am aware of only PIL and OpenCV for image analysis and therefore the format of my read images are confined to them both.

Related

Is there a way to get the hue value of an image using Python?

I am trying to build a machine learning algorithm where I need to convert pictures to their binaries. I am using Pillow library to get the data from images. Since the performance of the algorithm is not great, I need extra parameters to thoroughly train the network and one of the extra parameters might be hue.
So is there a method in Python that gives me hue value of an image?
I am not sure what you are really trying to do, as Christoph says in the comments, but you can find the mean hue of all the pixels in an image with PIL like this:
from PIL import Image, ImageStat
# Load image, convert to HSV and split the channels, retain H, discard S and V
H, _, _ = Image.open('image.png').convert('RGB').convert('HSV').split()
# Print the mean Hue across all pixels
print(ImageStat.Stat(H).mean)
Note that I converted to RGB first to avoid problems that may arise if you try this with palette images, CMYK images, images with transparency and greyscale images. See here.

Isolating the head in a grayscale CT image using Python

I am dealing with CT images that contain the head of the patient but also 'shadows' of the metalic cylinder.
These 'shadows' can appear down, left or right. In the image above it appears only on the lower side of the image. In the image below it appears in the left and the right directions. I don't have any prior knowledge of whether there is a shadow of the cylinder in the image. I must somehow detect it and remove it. Then I can proceed to segment out the skull/head.
To create a reproducible example I would like to provide the numpy array (128x128) representing the image but I don't know how to upload it to stackoverflow.
How can I achieve my objective?
I tried segmentation with ndimage and scikit-image but it does not work. I am getting too many segments.
12 Original Images
The 12 Images Binarized
The 12 Images Stripped (with dilation, erosion = 0.1, 0.1)
The images marked with red color can not help create a rectangular mask that will envelop the skull, which is my ultimate objective.
Please note that I will not be able to inspect the images one by one during the application of the algorithm.
You could use a combination of erosion (with an appropriate number of iterations) to remove the thin details, followed by dilation (also with an appropriate number of iterations) to restore the non-thin details to approximately the original size.
In code, this would look like:
import io
import requests
import numpy as np
import scipy as sp
import matplotlib as mpl
import PIL as pil
import scipy.ndimage
import matplotlib.pyplot as plt
# : load the data
url = 'https://i.stack.imgur.com/G4cQO.png'
response = requests.get(url)
img = pil.Image.open(io.BytesIO(response.content)).convert('L')
arr = np.array(img)
mask_arr = arr.astype(bool)
# : strip thin objects
struct = None
n_erosion = 6
n_dilation = 7
strip_arr = sp.ndimage.binary_dilation(
sp.ndimage.binary_erosion(mask_arr, struct, n_erosion),
struct, n_dilation)
plt.imshow(mask_arr, cmap='gray')
plt.imshow(strip_arr, cmap='gray')
plt.imshow(mask_arr ^ strip_arr, cmap='gray')
Starting from this image (mask_arr):
One would get to this image (strip_arr):
The difference being (mask_arr ^ strip_arr):
EDIT
(addressing the issues raised in the comments)
Using a different input image, for example a binarization of the input with a much lower threshold will help having larger and non-thin details of the head that will not disappear during erosion.
Alternatively, you may get more robust results by fitting an ellipse to the head.
Rather than "pure" image processing, like Ander Biguri above, I'd suggest maybe a different approach (actually two).
The concept here is to not rely on purely algorithmic image processing, but leverage the knowledge of the specifics of the situation you have:
1) Given the container is metal (as you stated) another approach that might be a lot easier is just thresholding, based on the specific HU number for the metal frame.
While you show the images as simple greyscale, in reality CT images are 16-bit images that are window levelled when viewed in a 256bit greyscale representation - so the pictures above are not a true representation of the full information available in the image data, which is actually 16 bit.
The metal frame would likely have a HU value that is significantly different to (higher than) anything within the anatomy. If that is the case, then a simple thresholding then subtraction would be a much simpler way to remove it.
2) Another approach would also be based on considering the geometry and properties of the specific situation you have:
In the images above, you could look at a vertical profile upwards in the middle of your image (column-wise) to find the location of the frame - with the location being the point that vertical profile crosses into a HU value that matches the frame.
From that point, you could use a flood fill approach (eg scikit flood_fill) to find all connected points within a certain tolerance.
That also would give you a set of points (mask) matching the frame that you could use to remove it from the original image.
I'm thinking that either of these approaches would be both faster and more robust for the situation you're proposing.

Otsu's method thresholding making a 'shroud'

I'm trying to threshold an image using Otsu's method in Opencv:
Although when I threshold it, some parts of the picture are completely surrounded by white and creates and ends up in Opencv not detecting all the contours in the image. This is what I get when I do Otsu's method thresholding usingret,thresh=cv2.threshold(blurred,0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU):
EDIT:
Some people have asked for the code I am using so here it is:
gray = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)
cv2.imshow('Input Image', image)
cv2.waitKey(0)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
thresh = cv2.adaptiveThreshold(blurred,255,cv2.ADAPTIVE_THRESH_GAUSSIAN_C,\
cv2.THRESH_BINARY_INV,81,2)
#ret, thresh = cv2.threshold(blurred,0,255,cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)
kernel = np.ones((5,5),np.uint8)
opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel)
closing = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel)
#thresh_value = 70
#ret,thresh= cv2.threshold(blurred,thresh_value,255,cv2.THRESH_BINARY)
Now it makes some checkered noise:
You do not need to manually find a sweet spot! Let OpenCV do it for you!
OpenCV has an adaptive thresholding algorithm exactly from problems like this, called adaptiveThreshold
This function divides the image into multiple sub-images, and thresholds each one individually. This means that it will find a nice threshold value for each part of the image and give you a nice and uniformly lit image. See this example.
Try this:
th3 = cv.adaptiveThreshold(blurred,255,cv.ADAPTIVE_THRESH_GAUSSIAN_C,\
cv.THRESH_BINARY,11,2)
Update:
Functions like these do not work perfectly out of the box. If it still creates artefacts like salt and pepper noise, you can try:
Significantly increasing the blockSize. This can ensure that each block has a letter inside, which will hopefully mean the threshold will be chosen better. (e.g. Dividing the image into 25 blocks instead of 100. A blocksize of 11 pixels is very small.)
First apply a blurring filter to ease out the bad spots creating the seasoning noise. (With the image name blurry I imagine that you've done this already.
First the simple threshold function to just removes some noise. For example setting all pixels above 5 and below 100 equal to zero. Then after that apply the adaptiveThreshold.
Follow #Mark`s advice by subtracting a blurred image from the original image. (See this thread)
I hope this helps!
Instead of using Otsu's method try global thresholding method.
thresh_value = 50
ret,thresh= cv2.threshold(blurred,thresh_value,255,cv2.THRESH_BINARY)
change the thresh_value parameter until you get the result you want.
Get to know more about thresholding techniques please refer the documentation.

Reducing / Enhancing known features in an image

I am microbiology student new to computer vision, so any help will be extremely appreciated.
This question involves microscope images that I am trying to analyze. The goal I am trying to accomplish is to count bacteria in an image but I need to pre-process the image first to enhance any bacteria that are not fluorescing very brightly. I have thought about using several different techniques like enhancing the contrast or sharpening the image but it isn't exactly what I need.
I want to reduce the noise(black spaces) to 0's on the RBG scale and enhance the green spaces. I originally was writing a for loop in OpenCV with threshold limits to change each pixel but I know that there is a better way.
Here is an example that I did in photo shop of the original image vs what I want.
Original Image and enhanced Image.
I need to learn to do this in a python environment so that I can automate this process. As I said I am new but I am familiar with python's OpenCV, mahotas, numpy etc. so I am not exactly attached to a particular package. I am also very new to these techniques so I am open to even if you just point me in the right direction.
Thanks!
You can have a look at histogram equalization. This would emphasize the green and reduce the black range. There is an OpenCV tutorial here. Afterwards you can experiment with different thresholding mechanisms that best yields the bacteria.
Use TensorFlow:
create your own dataset with images of bacteria and their positions stored in accompanying text files (the bigger the dataset the better).
Create a positive and negative set of images
update default TensorFlow example with your images
make sure you have a bunch of convolution layers.
train and test.
TensorFlow is perfect for such tasks and you don't need to worry about different intensity levels.
I initially tried histogram equalization but did not get the desired results. So I used adaptive threshold using the mean filter:
th = cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY_INV, 3, 2)
Then I applied the median filter:
median = cv2.medianBlur(th, 5)
Finally I applied morphological closing with the ellipse kernel:
k1 = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(5,5))
dilate = cv2.morphologyEx(median, cv2.MORPH_CLOSE, k1, 3)
THIS PAGE will help you modify this result however you want.

How to average a stack of images together using Pillow? [duplicate]

For example, I have 100 pictures whose resolution is the same, and I want to merge them into one picture. For the final picture, the RGB value of each pixel is the average of the 100 pictures' at that position. I know the getdata function can work in this situation, but is there a simpler and faster way to do this in PIL(Python Image Library)?
Let's assume that your images are all .png files and they are all stored in the current working directory. The python code below will do what you want. As Ignacio suggests, using numpy along with PIL is the key here. You just need to be a little bit careful about switching between integer and float arrays when building your average pixel intensities.
import os, numpy, PIL
from PIL import Image
# Access all PNG files in directory
allfiles=os.listdir(os.getcwd())
imlist=[filename for filename in allfiles if filename[-4:] in [".png",".PNG"]]
# Assuming all images are the same size, get dimensions of first image
w,h=Image.open(imlist[0]).size
N=len(imlist)
# Create a numpy array of floats to store the average (assume RGB images)
arr=numpy.zeros((h,w,3),numpy.float)
# Build up average pixel intensities, casting each image as an array of floats
for im in imlist:
imarr=numpy.array(Image.open(im),dtype=numpy.float)
arr=arr+imarr/N
# Round values in array and cast as 8-bit integer
arr=numpy.array(numpy.round(arr),dtype=numpy.uint8)
# Generate, save and preview final image
out=Image.fromarray(arr,mode="RGB")
out.save("Average.png")
out.show()
The image below was generated from a sequence of HD video frames using the code above.
I find it difficult to imagine a situation where memory is an issue here, but in the (unlikely) event that you absolutely cannot afford to create the array of floats required for my original answer, you could use PIL's blend function, as suggested by #mHurley as follows:
# Alternative method using PIL blend function
avg=Image.open(imlist[0])
for i in xrange(1,N):
img=Image.open(imlist[i])
avg=Image.blend(avg,img,1.0/float(i+1))
avg.save("Blend.png")
avg.show()
You could derive the correct sequence of alpha values, starting with the definition from PIL's blend function:
out = image1 * (1.0 - alpha) + image2 * alpha
Think about applying that function recursively to a vector of numbers (rather than images) to get the mean of the vector. For a vector of length N, you would need N-1 blending operations, with N-1 different values of alpha.
However, it's probably easier to think intuitively about the operations. At each step you want the avg image to contain equal proportions of the source images from earlier steps. When blending the first and second source images, alpha should be 1/2 to ensure equal proportions. When blending the third with the the average of the first two, you would like the new image to be made up of 1/3 of the third image, with the remainder made up of the average of the previous images (current value of avg), and so on.
In principle this new answer, based on blending, should be fine. However I don't know exactly how the blend function works. This makes me worry about how the pixel values are rounded after each iteration.
The image below was generated from 288 source images using the code from my original answer:
On the other hand, this image was generated by repeatedly applying PIL's blend function to the same 288 images:
I hope you can see that the outputs from the two algorithms are noticeably different. I expect this is because of accumulation of small rounding errors during repeated application of Image.blend
I strongly recommend my original answer over this alternative.
One can also use numpy mean function for averaging. The code looks better and works faster.
Here the comparison of timing and results for 700 noisy grayscale images of faces:
def average_img_1(imlist):
# Assuming all images are the same size, get dimensions of first image
w,h=Image.open(imlist[0]).size
N=len(imlist)
# Create a numpy array of floats to store the average (assume RGB images)
arr=np.zeros((h,w),np.float)
# Build up average pixel intensities, casting each image as an array of floats
for im in imlist:
imarr=np.array(Image.open(im),dtype=np.float)
arr=arr+imarr/N
out = Image.fromarray(arr)
return out
def average_img_2(imlist):
# Alternative method using PIL blend function
N = len(imlist)
avg=Image.open(imlist[0])
for i in xrange(1,N):
img=Image.open(imlist[i])
avg=Image.blend(avg,img,1.0/float(i+1))
return avg
def average_img_3(imlist):
# Alternative method using numpy mean function
images = np.array([np.array(Image.open(fname)) for fname in imlist])
arr = np.array(np.mean(images, axis=(0)), dtype=np.uint8)
out = Image.fromarray(arr)
return out
average_img_1()
100 loops, best of 3: 362 ms per loop
average_img_2()
100 loops, best of 3: 340 ms per loop
average_img_3()
100 loops, best of 3: 311 ms per loop
BTW, the results of averaging are quite different. I think the first method lose information during averaging. And the second one has some artifacts.
average_img_1
average_img_2
average_img_3
in case anybody is interested in a blueprint numpy solution (I was actually looking for it), here's the code:
mean_frame = np.mean(([frame for frame in frames]), axis=0)
I would consider creating an array of x by y integers all starting at (0, 0, 0) and then for each pixel in each file add the RGB value in, divide all the values by the number of images and then create the image from that - you will probably find that numpy can help.
I ran into MemoryErrors when trying the method in the accepted answer. I found a way to optimize that seems to produce the same result. Basically, you blend one image at a time, instead of adding them all up and dividing.
N=len(images_to_blend)
avg = Image.open(images_to_blend[0])
for im in images_to_blend: #assuming your list is filenames, not images
img = Image.open(im)
avg = Image.blend(avg, img, 1/N)
avg.save(blah)
This does two things, you don't have to have two very dense copies of the image while you're turning the image into an array, and you don't have to use 64-bit floats at all. You get similarly high precision, with smaller numbers. The results APPEAR to be the same, though I'd appreciate if someone checked my math.

Resources