PIL: scale image while maintaing highest possible quality - python

I'm using PIL to scale images that range anywhere from 600px wide to 2400px wide down to around 200px wide. I've already incorporated Image.ANTIALIAS and set quality=95 to try and get the highest quality image possible.
However the scaled down images still have pretty poor quality compared to the originals.
Here's the code that I'm using:
# Open the original image
fp = urllib.urlopen(image_path)
img = cStringIO.StringIO(fp.read())
im = Image.open(img)
im = im.convert('RGB')
# Resize the image
resized_image = ImageOps.fit(im, size, Image.ANTIALIAS)
# Save the image
resized_image_object = cStringIO.StringIO()
resized_image.save(resized_image_object, image_type, quality=95)
What's the best way to scale an image along these ratios while preserving as much of the image quality as possible?
I should note that my primary goal is get the maximum quality image possible. I'm not really concerned with how efficient the process is time wise.

If you can't get results with the native resize options in PIL, you can manually calculate the resize pixel values by running them through your own resizing function. There are three main algorithms (that I know of) for resizing images:
Nearest Neighbor
Bilinear Interpolation
Bicubic Interpolation
The last one will produce the highest quality image at the longest calculation time. To do this, imagine the pixel layout of the the smaller image, then scale it up to match the larger image and think about where the new pixel locations would be over the old ones. Then for each new pixel take the average value of the 16 nearest pixels (4x4 radius around it) and use that as its new value.
The resulting values for each of the pixels in the small image will be a smooth but clear resized version of the large image.
For further reading look here: Wikipedia - Bicubic interpolation

Try a different approach. I'm not sure if this will help, but I did something similar a while back:
https://stackoverflow.com/a/13211834/1339024
It may be that the original image on the urlpath is not that great quality to begin with. But if you want, try my script. I made it to shrink images in a given directory, but this portion could be of use:
parentDir = "Some\\Path"
width = 200
height = 200
cdpi = 75
cquality = 95
a = Image.open(parentDir+'\\'+imgfile) # Change this to your url type
iw,ih = a.size
if iw > width or ih > height:
pcw = width/float(iw)
pch = height/float(ih)
if pcw <= pch:
LPC = pcw
else:
LPC = pch
if 'gif' in imgfile:
a = a.convert("RGB")#,dither=Image.NONE)
a = a.resize((int(iw*LPC),int(ih*LPC)),Image.ANTIALIAS)
a = a.convert("P", dither=Image.NONE, palette=Image.ADAPTIVE)
a.save(outputDir+"\\"+imgfile,dpi=(cdpi,cdpi), quality=cquality)
else:
a = a.resize((int(iw*LPC),int(ih*LPC)),Image.ANTIALIAS)
a.save(outputDir+"\\"+imgfile,dpi=(cdpi,cdpi), quality=cquality)

Related

Convert pixels to cm using Python

I would like to know what am I doing wrong with this code :
if self.digital:
im = Image.open(os.path.join(folder, filename))
width, height = im.size
image_info["width"] = round(width / 37.79527559055, 0)
I would like to use this code to convert the pixel size of a picture into centimeters, but I don't understand why it returns me this issue :
Python311\Lib\site-packages\PIL\Image.py:3167: DecompressionBombWarning: Image size (130437549 pixels) exceeds limit of 89478485 pixels, could be decompression bomb DOS attack.
I don't want to use DPI, in my script 1cm = 37.79527559055 pixels.
I'm going to use a temporary list to write pixels value in then convert but I would like to know if there is a faster way or not, and why exactly is it making a zip bomb.
Thanks !
The issue is that the image you are trying to open is too large and its decompression would consume a lot of memory, which can be a potential security risk (known as "Decompression bomb"). To avoid this, you can increase the maximum size limit of the image in PIL by modifying the Image.MAX_IMAGE_PIXELS attribute:
Image.MAX_IMAGE_PIXELS = None # No Limit
It is important to consider the memory usage and performance of your script when using large images.
Regarding the conversion of pixels to centimeters, using a fixed number of pixels per centimeter is not the recommended approach as it depends on the display resolution and the physical size of the display. Instead, you should use the DPI (dots per inch) information embedded in the image file to perform the conversion. You can retrieve the DPI information using the 'info' attribute of the Image object and then use it to calculate the size in centimeters.
Here is an example of how you could do the conversion with DPI information:
from PIL import Image
im = Image.open(os.path.join(folder, filename))
width, height = im.size
dpi = im.info.get("dpi", (72, 72))
width_cm = width / dpi[0] * 2.54
height_cm = height / dpi[1] * 2.54
image_info["width_cm"] = width_cm
image_info["height_cm"] = height_cm
You can resize the image to a smaller size before processing it. You can use the Image.thumbnail() method provided by PIL to resize the image.

Ignore image name while getting hash

I'm coding a program which'll take an image for an input, check it against images in a database and output the image with the same hash
However, when using hash("imagepath") 2 of the same images give different hashes, even when the only difference is the image's name, which makes me believe the name is the issue
Is there a way to easily ignore the name of the image? (png)
How I solved it:
I ended up not using "hashing" but the average pixel by scrambeling pieces of code together, and then find an image with the same average pixel (the average pixels are in a list so it gets the index which it then uses to find a name)
import requests
#Database of possible image average pixels
clone_imgs = [88.0465, 46.2568, 102.6426 ...]
image = <image url>
img_data = requests.get(image).content
with open('image.png', 'wb') as handler: #Download the image as "image.png" (Replace "image.png" with the path where you want to save it)
handler.write(img_data)
img = Image.open(r"image.png") #Open the image for reading
img = img.resize((100, 100), Image.ANTIALIAS) #A series of compressions to the image
img = img.convert("L")
img_pixel_data = list(spawn.getdata())
img_avg_pixel = sum(spawn_pixel_data)/len(spawn_pixel_data) #Get the average pixel values
clone_img_index = clone_imgs.index(img_avg_pixel) #Find the same pixel value in the database
This worked for me but it has a few downsides:
The images need to be 100% the same in color (A single pixel off can ruin it)
One of these average pixels can make an infinite amount of images, my database only contained 800 so it still worked (However I had to go from compression to 10x10 to 100x100 to not end up with clones)

Python: Normalize image exposure

I'm working on a project to measure and visualize image similarity. The images in my dataset come from photographs of images in books, some of which have very high or low exposure rates. For example, the images below come from two different books; the one on the top is an over-exposed reprint of the one on the bottom, wherein the exposure looks good:
I'd like to normalize each image's exposure in Python. I thought I could do so with the following naive approach, which attempts to center each pixel value between 0 and 255:
from scipy.ndimage import imread
import sys
def normalize(img):
'''
Normalize the exposure of an image.
#args:
{numpy.ndarray} img: an array of image pixels with shape:
(height, width)
#returns:
{numpy.ndarray} an image with shape of `img` wherein
all values are normalized such that the min=0 and max=255
'''
_min = img.min()
_max = img.max()
return img - _min * 255 / (_max - _min)
img = imread(sys.argv[1])
normalized = normalize(img)
Only after running this did I realize that this normalization will only help images whose lightest value is less than 255 or whose darkest value is greater than 0.
Is there a straightforward way to normalize the exposure of an image such as the top image above? I'd be grateful for any thoughts others can offer on this question.
Histogram equalisation works surprisingly well for this kind of thing. It's usually better for photographic images, but it's helpful even on line art, as long as there are some non-black/white pixels.
It works well for colour images too: split the bands up, equalize each one separately, and recombine.
I tried on your sample image:
Using libvips:
$ vips hist_equal sample.jpg x.jpg
Or from Python with pyvips:
x = pyvips.Image.new_from_file("sample.jpg")
x = x.hist_equal()
x.write_to_file("x.jpg")
It's very hard to say if it will work for you without seeing a larger sample of your images, but you may find an "auto-gamma" useful. There is one built into ImageMagick and the description - so that you can calculate it yourself - is:
Automagically adjust gamma level of image.
This calculates the mean values of an image, then applies a calculated
-gamma adjustment so that the mean color in the image will get a value of 50%.
This means that any solid 'gray' image becomes 50% gray.
This works well for real-life images with little or no extreme dark
and light areas, but tend to fail for images with large amounts of
bright sky or dark shadows. It also does not work well for diagrams or
cartoon like images.
You can try it out yourself on the command line very simply before you go and spend a lot of time coding something that may not work:
convert Tribunal.jpg -auto-gamma result.png
You can do -auto-level as per your own code beforehand, and a thousand other things too:
convert Tribunal.jpg -auto-level -auto-gamma result.png
I ended up using a numpy implementation of the histogram normalization method #user894763 pointed out. Just save the below as normalize.py then you can call:
python normalize.py cats.jpg
Script:
import numpy as np
from scipy.misc import imsave
from scipy.ndimage import imread
import sys
def get_histogram(img):
'''
calculate the normalized histogram of an image
'''
height, width = img.shape
hist = [0.0] * 256
for i in range(height):
for j in range(width):
hist[img[i, j]]+=1
return np.array(hist)/(height*width)
def get_cumulative_sums(hist):
'''
find the cumulative sum of a numpy array
'''
return [sum(hist[:i+1]) for i in range(len(hist))]
def normalize_histogram(img):
# calculate the image histogram
hist = get_histogram(img)
# get the cumulative distribution function
cdf = np.array(get_cumulative_sums(hist))
# determine the normalization values for each unit of the cdf
sk = np.uint8(255 * cdf)
# normalize the normalization values
height, width = img.shape
Y = np.zeros_like(img)
for i in range(0, height):
for j in range(0, width):
Y[i, j] = sk[img[i, j]]
# optionally, get the new histogram for comparison
new_hist = get_histogram(Y)
# return the transformed image
return Y
img = imread(sys.argv[1])
normalized = normalize_histogram(img)
imsave(sys.argv[1] + '-normalized.jpg', normalized)
Output:

Removing grid from an image

I'm working on a project that lets user take photos of handwritten formulas and send them to my server. I want to leave only symbols related to mathematics, not sheet grid.
Sample photo:
(1) Original RGB photo
(2) Blurred Grayscale
(3) After applying Adaptive Threshold
NOTE:
I expect my algorithm to deal with sheet grid of any color.
Any code snippets will be greatly appreciated.
Thanks in advance.
Result
This is a challenging problem to generalize without knowing exactly what kind of paper/lines and ink combination to expect, and what exactly the output will be used for. I'd thought I'd attempt it and maybe learn something.
I see two ways to approach this problem:
The clever way: identify the grid, its color, orientation, size to find the regions of the image occupied by it, in order to ignore it. There are major caveats here that would need to be addressed. e.g. the page may not be photographed flat and squared (warp, distortion, rotation have to accounted for). There will also be lines that we don't want removed.
The simple way: Apply general image manipulations, knowing little about the problem other than the assumptions that the pen is always darker than the grid, and the output is to be binary (black pen / white page).
I like the second one better because it is easier to implement and generalizes better.
We first notice that the "white" of the page is actually a non-uniform shade of grey (if we convert to grayscale). The CV adaptive thresholding deals with this nicely. It almost gets us there.
The code below treats the image in 50x50 pixel blocks to address the non-uniformity of lighting. In each block, we subtract the median before applying a threshold. A simple solution, but maybe what you need. I haven't tested it on many images and the threshold and pre- and post-processing may need tweaking. It will not work if input images vary significantly, or if the grid is too dark relative to the ink.
import cv2
import numpy
import sys
BLOCK_SIZE = 50
THRESHOLD = 25
def preprocess(image):
image = cv2.medianBlur(image, 3)
image = cv2.GaussianBlur(image, (3, 3), 0)
return 255 - image
def postprocess(image):
image = cv2.medianBlur(image, 5)
# image = cv2.medianBlur(image, 5)
# kernel = numpy.ones((3,3), numpy.uint8)
# image = cv2.morphologyEx(image, cv2.MORPH_OPEN, kernel)
return image
def get_block_index(image_shape, yx, block_size):
y = numpy.arange(max(0, yx[0]-block_size), min(image_shape[0], yx[0]+block_size))
x = numpy.arange(max(0, yx[1]-block_size), min(image_shape[1], yx[1]+block_size))
return numpy.meshgrid(y, x)
def adaptive_median_threshold(img_in):
med = numpy.median(img_in)
img_out = numpy.zeros_like(img_in)
img_out[img_in - med < THRESHOLD] = 255
return img_out
def block_image_process(image, block_size):
out_image = numpy.zeros_like(image)
for row in range(0, image.shape[0], block_size):
for col in range(0, image.shape[1], block_size):
idx = (row, col)
block_idx = get_block_index(image.shape, idx, block_size)
out_image[block_idx] = adaptive_median_threshold(image[block_idx])
return out_image
def process_image_file(filename):
image_in = cv2.cvtColor(cv2.imread(filename), cv2.COLOR_BGR2GRAY)
image_in = preprocess(image_in)
image_out = block_image_process(image_in, BLOCK_SIZE)
image_out = postprocess(image_out)
cv2.imwrite('bin_' + filename, image_out)
if __name__ == "__main__":
process_image_file(sys.argv[1])
OpenCV has a tutorial dealing with removing grid from an image:
"Extract horizontal and vertical lines by using morphological operations", OpenCV documentation,
source : https://docs.opencv.org/master/dd/dd7/tutorial_morph_lines_detection.html
This is a pretty difficult task. I also had this problem and I discovered that the solution can't be 100% accurate. BTW, just a few days ago I saw this link. Maybe it could help.

Finding coordinates of brightest pixel in an image and entering them into an array

I have been asked to write a program to find 'stars' in an image by converting the image file to a numpy array and generating an array of the coordinates of the brightest pixels in the image above a specified threshold (representing background interference).
Once I have located the brightest pixel in the image I must record its x,y coordinates, and set the value of that pixel and surrounding 10X10 pixel area to zero, effectively removing the star from the image.
I already have a helper code which converts the image to an array, and have attempted to tackle the problem as follows;
I have defined a variable
Max = array.max()
and used a while loop;
while Max >= threshold
coordinates = numpy.where(array == Max) # find the maximum value
however I want this to loop over the whole array for all of the coordinates,not just find the first maximum, and also remove each maximum when found and setting the surrounding 10X10 area to zero. I have thought about using a for loop to do this but am unsure how I should use it since I am new to Python.
I would appreciate any suggestions,
Thanks
There are a number of different ways to do it with just numpy, etc.
There's the "brute force" way:
import Image
import numpy as np
im = Image.open('test.bmp')
data = np.array(im)
threshold = 200
window = 5 # This is the "half" window...
ni, nj = data.shape
new_value = 0
for i, j in zip(*np.where(data > threshold)):
istart, istop = max(0, i-window), min(ni, i+window+1)
jstart, jstop = max(0, j-window), min(nj, j+window+1)
data[istart:istop, jstart:jstop] = new_value
Or the faster approach...
import Image
import numpy as np
import scipy.ndimage
im = Image.open('test.bmp')
data = np.array(im)
threshold = 200
window = 10 # This is the "full" window...
new_value = 0
mask = data > threshold
mask = scipy.ndimage.uniform_filter(mask.astype(np.float), size=window)
mask = mask > 0
data[mask] = new_value
Astronomy.net will do this for you:
If you have astronomical imaging of the sky with celestial coordinates
you do not know—or do not trust—then Astrometry.net is for you. Input
an image and we'll give you back astrometric calibration meta-data,
plus lists of known objects falling inside the field of view.
We have built this astrometric calibration service to create correct,
standards-compliant astrometric meta-data for every useful
astronomical image ever taken, past and future, in any state of
archival disarray. We hope this will help organize, annotate and make
searchable all the world's astronomical information.
You don't even have to upload the images to their website. You can download the source. It is licensed under the GPL and uses NumPy, so you can muck around with it if you need to.
Note that you will need to first convert your bitmap to one of the following: JPEG, GIF, PNG, or FITS image.

Categories