SteveyyP
SteveyyP

Reputation: 1

PIL create blank images when cropping

I'm working with image files that are ~50MB (~19000 pixels x 25500 pixels) and cropping them into images of size 4705 pixels x 8375 pixels. I wrote an for-loop that iterates through a folder of 95 images. For the most part, the cropping works fine, but on random images, when the code crops the image, it sub-image comes out as a blank image. When this occurs, the first of the 12 images will come out normal (cropped correctly), but the remaining 11 images will come out blank. When the issue does not happen all 12 images come out cropped correctly.

I'm running the code on Spyder 3.3.4 on a MBP 10.14.5. PIL is version 1.1.7 and Python 3.6. I've checked that I'm looping correctly across the images. Rerunning images that have failed (cropped incorrectly), work fine when I crop them on their on and not a part of a for loop.

stepCounter = 4705

for folder in os.listdir(location):
    if folder == "MyFolder":
        for file in os.listdir(location+folder):
            resetCounter = -8375
            for i in range(12):
                print("Iteration", i, " on file", file)
                if i%4 == 0:
                    resetCounter += 8375
                    left = 0 
                    top = 0 + resetCounter
                    right = 4705
                    bottom = 8375 + resetCounter
                    fileLocation = location + folder + "/" + file
                    newLocation = location + folder + "/" + file[:-4] + str(i+1) + ".jpg"
                    img = Image.open(fileLocation)
                    img = img.crop((left, top, right, bottom))
                    img.save(newLocation)
                    img.close()
                else:
                    left = left + stepCounter
                    top = top 
                    right = right + stepCounter
                    bottom = bottom
                    fileLocation = location + folder + "/" + file
                    newLocation = location + folder + "/" + file[:-4] + str(i+1) + ".jpg"
                    img = Image.open(fileLocation)
                    img = img.crop((left, top, right, bottom))
                    img.save(newLocation)
                    img.close()
    else:
        print("Skipping", folder)

Again, I expect the images to be sub-images of the larger image, not blank images. Not sure if this is a memory issue, or something else not code related.

Upvotes: 0

Views: 3307

Answers (1)

jsbueno
jsbueno

Reputation: 110631

It is hard to tell from looking the program - it would work if each image was like you were describing - however, your code for naming the target images is not using programming patterns that are error proof, as they are not taking the best advantage of some of the language facilities. That code works now, but there might have been some trial and error to get there. So, my bet is that at some point in time, an incorrect version of this script was run, that had an incorrect behavior on generating the target slice files. This run did overwrite some of the images, which now are the size of a single slice.

Actually, if one calls the crop method beyond the images pixel sizes on a PIL image object, no error is raised: a zeroed (black) image is silently created instead.

You did not mention, but if you will check the images for which the slicing is failing now, the case is that your originals are likely already cropped to the smaller size.

Also, as there are no checks for which images you are cropping, if you run this code more than once, the crops already saved will be processed as if they were big images again.

That is, on the first run of this script, an "image.jpg" would be saved and cropped to "image1.jpg" through "image12.jpg" - but on the second run, each of these "imageN.jpg" would become "imageNM.jpg" - with "M" going again from "1" to "12". Also, the 11th and 12th images of the first run "image11.jpg" and "image12.jpg" would be replaced by the first and second outputs of the second run.

So, if you can still restore your original folders with the images that are exactly 25500 x 19000 pixels, and only those, a refactored version of this code could be run, that would ensure not to reprocess the slices already made. A single check for the image width can avoid that and a more explicit naming schema could also be better.

Also, as some coding advice:

  • make use of Python's "f-strings" for manipulating the names,
  • Use Python's pathlib.Path to manipulate folder names and get to image files (this is new from Python 3.5, and there are sparse examples around),
  • avoid hardcoded numbers all around the code - just put then on the beginning of the listing, as variables
  • Use explicit iterations on x and y, instead of a linear counter and then some error-prone arithmetic to get to the limits to be cropped
  • Finally, as I mentioned above, take care not to re-read the same images several times over, the script becomes much easier on the eye, and less error prone.

There is also a chance you really did hit a bug in PIL, due to the large image sizes, and the big images are failing to be loaded from the second-time on - but that is rather unlikely. A problem with this would rather stop the program with a MemoryError.

import pathlib
from PIL import Image

# Whatever code you have to get the "location" variable
...

x_step = 8375
y_step = 4705

full_width = 25500

for image_path in pathlib.Path(location).glob("**/*.jpg"):
    # the Path.glob method automatically iterates in subfolders, for
    # all files matching the expressions
    if "MyFolder" not in image_path.parts:
        # Skips processing if "MyFolder" not in the relative path to the image file
        continue
    # Loads the original image a single time:
    img = Image.open(image_path)
    if img.width < full_width:
        if "crop" not in image_path.name:
            # Do not print warnings for slices - just skip then
            print(f"Image at {image_path} has width of only {img.width}. Skipping")
        continue
    for y in range(3):
        for x in range(4):
            print(f"Iteration {y * 4 + x} on file {image_path.name}")
            # Store the cropped image object into a new variable - the original image is kept on "img"
            left = x_step * x
            top = y_step * y
            sliced_img = img.crop((left, top, left + x_step, top + y_step))
            new_path = image_path.with_name(f"{image_path.stem}_crop_{y * 4 + x + 1}{image_path.suffix}")
            sliced_img.save(new_path)

Upvotes: 1

Related Questions