I have a png image that I want to crop, removing the top and bottom white space.
I use the following code:
from PIL import Image
for f in pa_files:
img = f
im = Image.open(img)
width, height = im.size
pixels = list(im.getdata())
pixels = [pixels[i * width:(i 1) * width] for i in range(height)]
white_lines = 0
for line in pixels:
white_count = sum([sum(x) for x in line]) - im.width * 255*4
if (white_count) == 0:
white_lines = 1
else:
break
crop_from_top = white_lines
pixels.reverse()
white_lines = 0
for line in pixels:
white_count = sum([sum(x) for x in line]) - im.width * 255*4
if (white_count) == 0:
white_lines = 1
#print(white_count)
else:
break
crop_from_bottom = white_lines
crop_from_bottom, crop_from_top, im.size
# Setting the points for cropped image
left = 0
top = crop_from_top - 5
right = im.width
bottom = im.height - (crop_from_bottom- 5)
im1 = im.crop((left, top, right, bottom))
im1.save(img)
this works for a 32 bit png
but now I come across an 8 bit png, and tried running the same script, but came across this error:
TypeError: 'int' object is not iterable
Looking further, I see that each pixel is represented by 0:255
and we see pixel value 153 appears 2m times.
I played around cropping with the following:
im = Image.open(f).convert('L')
im = im.crop((x1, y1, x2, y2))
im.save('_0.png')
successfully, but then my image returned grayscale.
after:
it went from blue to grayscale.
How is it possible to crop the margins dynamically of an 8bit type image, and save it again in colour?
CodePudding user response:
The thing is that you have to consider many different cases.
- 8 bits R,G,B,A images (that is what you have, apparently, at first)
- 8 bits R,G,B images
- 8 bits gray level
- 8 bits indexed images
For 8 bits gray level, pixels are not 4-uplets (R,G,B,A) but numbers. So, sum(x)
should be replaced by x
. And then you can expect it to worth 255, not 255*4
for white (but that is not a sure thing neither. There are some 'MINISWHITE' format also. Since I don't have an example, and not very familiar with PIL (that you are obviously using), can't be sure if PIL would make this transparent (I mean, if it would convert it when loading).
For example in the 1st part of your code
white_lines = 0
for line in pixels:
white_count = sum([x for x in line]) - im.width * 255
if (white_count) == 0:
white_lines = 1
else:
break
For R,G,B image, your code would be OK, but white is not when sum is 255*4
, but 255*3
.
Your second example is an indexed image. So 8 bits, but color anyway. By converting it to 'L', that is gray level, you got what you are complaining about.
So, the simple answer here would be to advise you to convert everything to RGB
or RGBA
and then use your code.
for f in pa_files:
img = f
im = Image.open(img)
width, height = im.size
pixels = list(im.convert('RGBA').getdata())
pixels = [pixels[i * width:(i 1) * width] for i in range(height)]
# Rest of code unchanged
im1 = im.crop((left, top, right, bottom))
im1.save(img)
The conversion would not impact the image (it won't convert a gray level image in a stupid RGBA image whose all R=G=B, wasting space), since the conversion is only to get pixel array used for computation of crop area, and crop is performed on the original unconverted image.
I can't resist the urge to advise you to avoid iterating over pixels at all cost, tho.
You could, instead of creating a python list (that you have to reshape yourself), get a numpy array view of the data.
import numpy as np
arr=np.asarray(im.convert('RGB'))
Then, to check if a line i
is white
LineIisWhite=(arr[i]==255).all()
(arr[i]==255)
is an array of booleans, of the same shape of your line, that is, here W×3, with True where there where 255, and False elsewhere.
(arr[i]==255).all()
is a boolean saying whether all boolean in previous arrays are True or not. So if line is white.
That still wouldn't avoid an iteration over lines. But we can do better.
Restricting all
to the 2 last axis (W and 3), by adding axis=(1,2)
and applying on the whole image, we get an array of H booleans, that are True if all W×3 booleans are true in each line.
whitelines=(arr==255).all(axis=(1,2))
In the example image I build, that result in
array([ True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True,
True, True, True, False, False, False, False, False, False,
False, False, False, False, False, False, False, False, False,
False, False, False, False, False, False, False, False, False,
False, False, False, False, False, False, False, False, False,
False, False, False, False, False, False, False, False, False,
False, False, False, False, False, False, False, False, False,
False, False, False, False, False, False, False, False, False,
False, False, False, False, False, False, False, False, False,
False, False, False, False, False, False, False, False, False,
False, False, False, False, False, False, False, False, False,
False, False, False, False, False, False, False, False, False,
False, False, False, False, False, False, False, False, False,
False, False, False, False, False, False, False, True, True,
True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True,
True, True, True])
Then, no need to iterate over this array of booleans to count number of True
at the beginning or at the end
crop_from_top = np.argmin(whitelines)
crop_from_bottom = np.argmin(whitelines[::-1])
So, altogether
import numpy as np
for f in pa_files:
img = f
im = Image.open(img)
width, height = im.size
arr = np.asarray(im.convert('RGB'))
whitelines=(arr==255).all(axis=(1,2))
crop_from_top = np.argmin(whitelines)
crop_from_bottom = np.argmin(whitelines[::-1])
# Setting the points for cropped image
left = 0
top = crop_from_top - 5
right = im.width
bottom = im.height - (crop_from_bottom- 5)
im1 = im.crop((left, top, right, bottom))
im1.save(img)
Last remark: because of indexation that has to approximate colors, or even because of JPEG encoding, white pixels may not be purely white.
So you may want to change arr==255
by arr>=250
or something like that.
Note that the numpy array here is used read-only. We only use it to compute how many lines to crop.
CodePudding user response:
We may simply convert every input image to RGB using im = im.convert('RGB')
:
im = Image.open(img)
if im.mode != 'RGBA': # Keep RGBA (32 bits) images unmodified
im = im.convert('RGB')
...
- If the input image is RGB, it is not going to change.
- If the image is indexed image (has say 8 bits per pixel) and palette, the image is converted to RGB.
- If the image is 8 bits grayscale, it is also converted to RGB (where R=G=B for each pixel).
Code sample:
from PIL import Image
file_name = 'indexed_image.png' # file_name = 'rgb_image.png' # file_name = 'gray_image.png'
im = Image.open(file_name)
if im.mode != 'RGBA': # Keep RGBA images unmodified
im = im.convert('RGB') # Convert indexed image to RGB image if required (pass each pixel through the palette).
width, height = im.size
pixels = list(im.getdata())
pixels = [pixels[i * width:(i 1) * width] for i in range(height)]
white_lines = 0
for line in pixels:
white_count = sum([sum(x) for x in line]) - im.width * 255*4
if (white_count) == 0:
white_lines = 1
else:
break
crop_from_top = white_lines
pixels.reverse()
white_lines = 0
for line in pixels:
white_count = sum([sum(x) for x in line]) - im.width * 255*4
if (white_count) == 0:
white_lines = 1
#print(white_count)
else:
break
crop_from_bottom = white_lines
crop_from_bottom, crop_from_top, im.size
# Setting the points for cropped image
left = 0
top = crop_from_top - 5
right = im.width
bottom = im.height - (crop_from_bottom- 5)
im1 = im.crop((left, top, right, bottom))
im1.save(file_name)
Sample images (used for testing):