Home > Enterprise >  how to crop a colour 8 bit per pixel png image and save in colour in python
how to crop a colour 8 bit per pixel png image and save in colour in python

Time:11-26

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

enter image description here

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

enter image description here

Looking further, I see that each pixel is represented by 0:255 enter image description here

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.

before: enter image description here

after:

enter image description here

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

RGB:
enter image description here

Indexed image:
enter image description here

Grayscale image:
enter image description here

RGBA image:
enter image description here

  • Related