Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

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

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:

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

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

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading