"UnboundLocalError: …" after creating objects within nested control blocks

Advertisements

I am writing a Python module which removes the background from animated GIF images. My problem function takes a directory path string and a URL string as input. The function uses an IF statement to pick either the URL or the path, based on if the URL is empty ("") or not. Pseudo-code is basically:

If URL != "", then get URL content and create a PIL.Image, else, search the directory path for gif and use to create PIL.Image.

In either case, a PIL.Image object called "im" is created and then "im" is referenced again after the IF block, which is where I get the error:

UnboundLocalError: local variable 'im' referenced before assignment

I thought that control blocks like IFs, Fors, Whiles, etc. were exempt from the variable scope restrictions that separate function variables from each other, but I am clearly missing something.

Below is my module code. I have put indicator comments around what I believe is the problem area (within function "gif_to_imgs").

src/gif_to_imgs.py

from PIL import Image
import os
import tempfile
import shutil
from rembg import remove
import glob
import contextlib
import requests
from io import BytesIO


def remove_img_bg(img_file, output_path):
    output = remove(img_file)
    output.save(output_path)
    print(f"Background removed, output saved to:    \"{output_path}\"")


def imgs_to_video(framesDir):
    # https://stackoverflow.com/a/57751793/17312223
    fp_in = f"{framesDir}/frame-*.png"  # Will return all file paths obeying regex to a list var
    fp_out = f"{framesDir}/modified.gif"

    # use exit stack to automatically close opened images
    with contextlib.ExitStack() as stack:
        # lazily load images
        imgs = (stack.enter_context(Image.open(f))
                for f in sorted(glob.glob(fp_in)))

        # extract  first image from iterator
        img = next(imgs)

        # https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html#gif
        img.save(fp=fp_out, format='GIF', disposal=2, append_images=imgs,
                 save_all=True, duration=100, loop=0)


def gif_to_imgs(imgDir,gif_url):
    framesDir = f"{imgDir}/gif_frames"

    # Delete frames folder if exists
    if (os.path.exists(framesDir)):
        tmp = tempfile.mktemp(dir=os.path.dirname(framesDir))
        shutil.move(framesDir, tmp)  # "Move" to temporary folder
        shutil.rmtree(tmp)  # Delete
    
    os.mkdir(framesDir)

    #==============================================================
    # PROBLEM BEGINS HERE
    #==============================================================
    
    if (gif_url != ""):
        response = requests.get(gif_url)
        im = Image.open(BytesIO(response.content))
    else:
        for filename in os.listdir(imgDir):
            if (filename == "*.gif"):
                im = Image.open(f"{imgDir}/{filename}.gif")

    print("Number of frames: " + str(im.n_frames))    #<---- "im" causes the error here
    
    #==============================================================
    # CODE FAILS AT THE ABOVE "PRINT()"
    #==============================================================

    for i in range(0, im.n_frames):
        # Create sort-friendly numerical suffix for GIF frames
        numSuffix = str(i)
        while (len(numSuffix) < 4):
            numSuffix = f"0{numSuffix}"

        # Remove background from the frame
        im.seek(i)  # Iterate to specific frame
        duration = im.info['duration']/1000  # Get frame duration
        output_path = f"{framesDir}/frame-{numSuffix}-{duration}.png"  # Set output path
        remove_img_bg(im, output_path)  # Remove background

    imgs_to_video(framesDir)

In case you are curious where the entry point for this module is, see my main.py script that calls it below:

main.py

from src import gif_to_imgs


if __name__ == '__main__':

    # gif_url = "https://media.tenor.com/tX_T48A14BwAAAAd/khaby-really.gif"
    gif_url = ""

    # takes path and url as input
    gif_to_imgs.gif_to_imgs("assets", gif_url)

UPDATED – SOLUTION:

I will try to explain how I fixed this. Per @Samwise’s insight that the IF statement would not initialize "im" in all cases, I changed it as follows:

  • Added new IF preceding original IF which checks if local gif file exists and assigns to "im2" object if found
  • Then, updated original IF to first check URL is not blank, if blank, check if "im2" exists/True, and finally, if neither case is true, it prints an error message and exits the program

Here is the updated code (just the function containing the error spot):

def gif_to_imgs(imgDir,gif_url):
    framesDir = f"{imgDir}/gif_frames"

    # Delete frames folder if exists
    if (os.path.exists(framesDir)):
        tmp = tempfile.mktemp(dir=os.path.dirname(framesDir))
        shutil.move(framesDir, tmp)  # "Move" to temporary folder
        shutil.rmtree(tmp)  # Delete

    # Check if local GIF exists in path
    for filename in os.listdir(imgDir):
        if filename.lower().endswith(".gif"):
            im2 = Image.open(f"{imgDir}/{filename}")
            break
        else:
            im2 = False

    os.mkdir(framesDir)
    if (gif_url != ""):
        response = requests.get(gif_url)
        im = Image.open(BytesIO(response.content))
    elif im2:
        im = im2
    else:
        print("no URL given and no local GIF file detected - please try again")
        sys.exit(1)

    print("Number of frames: " + str(im.n_frames))

    for i in range(0, im.n_frames):
        # Create sort-friendly numerical suffix for GIF frames
        numSuffix = str(i)
        while (len(numSuffix) < 4):
            numSuffix = f"0{numSuffix}"

        # Remove background from the frame
        im.seek(i)  # Iterate to specific frame
        duration = im.info['duration']/1000  # Get frame duration
        output_path = f"{framesDir}/frame-{numSuffix}-{duration}.png"  # Set output path
        remove_img_bg(im, output_path)  # Remove background

    imgs_to_video(framesDir)

>Solution :

This block of code won’t always initialize im:

    if (gif_url != ""):
        response = requests.get(gif_url)
        im = Image.open(BytesIO(response.content))
    else:
        for filename in os.listdir(imgDir):
            if (filename == "*.gif"):
                im = Image.open(f"{imgDir}/{filename}.gif")

Specifically, in the case where there is no gif_url and there are no files called *.gif in imgDir.

Since the Python equality operator doesn’t understand filesystem wildcards, you probably meant for this:

            if (filename == "*.gif"):

to be something more like:

            if filename.lower().endswith(".gif"):

but you should still probably think about how to handle the case where no files satisfy that condition (or maybe if multiple files do — your code assumes that there will always be exactly one image, so if you find multiples, it’ll ignore all but the last one).

Leave a ReplyCancel reply