8
\$\begingroup\$

The code below takes two command line arguments, an absolute folder path and integer, which is the minimum number of qualifying photos in each folder required to return a positive search result.

Saves/prints to screen a dictionary containing the path of the photo containing folders and the paths of the images within them. The search is recursive.

I thought this could be useful as a step towards grabbing all those forgotten photos on my computers and those of relatives etc(all I do here is fetch each photo's location).

#! python3

# Recursively searches the given folder for photos which meet the defined
# criteria
# Prints search results to screen and saves said data as a pickled object and
# as a reader friendly .txt file using pprint

import argparse
import logging
import os
import pickle
import pprint
from PIL import Image

logging.basicConfig(level=logging.INFO, format="%(asctime)s - " +\
                    "%(levelname)s - %(message)s")
logging.disable(logging.CRITICAL)


def check_size(images: list, min_dimension: int):
    """Returns a list of all photos which meet dimension requirements

    """
    photos = [image for image in images if min(Image.open(image).size) >
              min_dimension]
    return photos


def find_images(filenames: list, folder: str, formats: list):
    """Returns list of image abspaths for a folder if format in 'formats'"""

    all_images = [os.path.join(folder, file) for file in filenames if
                  file.lower().endswith(tuple(formats))]
    return all_images


def handle_input():
    """Parses and returns user input with guidance to correct input format"""

    parser = argparse.ArgumentParser()
    parser.add_argument("folder", type=str,
                        help="the folder (this folder's subfolders will " +
                        "also be searched) to search for photo files.")
    parser.add_argument("min_num_photos", type=int,
                        help="The minimum number of photos required to be " +
                        "present in a folder for that folders's data to be " +
                        "saved and displayed.")
    args = parser.parse_args()
    return args.folder, args.min_num_photos


def photo_search(folder: str, min_photos: int):
    """Return data structure of all folders with 'min_photos' photos

    """

    # photo formats
    formats = ["jpg", "png"]

    # minimum qualifying size of any dimension(height, width)
    min_dimension = 500

    # store positive results of search in dict data structure
    data = {}

    for folder, subfolders, filenames in os.walk(folder):

        # get list of all images in folder
        all_images = find_images(filenames, folder, formats)
        logging.info("all_images: {}".format(all_images))

        # get a list of all photos present in folder
        photos = check_size(all_images, min_dimension)
        logging.info("photos: {}".format(photos))

        # append data to data dict if min requirements met
        if len(photos) > min_photos:
            #folder_dict = {"photos": photos, "number": len(photos)}
            data[folder] = {"photos": photos, "number": len(photos)}
    return data


def save_data(data: dict, folder: str):
    """Saves a data dict to file as pickled obj and readable .txt file"""

    # save pickled object
    with open(os.path.join(folder, "results.dat"), "wb") as f:
        pickle.dump(data, f)

    # save reader friendly text version of data
    with open(os.path.join(folder, "results.txt"), "w") as f:
        f.write(pprint.pformat(data))


def main():
    """Performs a recursive search of a folder for photos meeting a defined
       criteria
    """

    # save search results to following folder
    save_folder = r"C:\Users\Dave\Desktop\2016Coding\AutomateBoring" +\
                  r"\17-ManipulatingImages\photo_search"

    # if save folder doesn't exist, create
    if not os.path.exists(save_folder):
        os.mkdir(save_folder)

    folder, min_photos = handle_input()

    # perform a search of folder for photo files by walking the folder tree
    data = photo_search(folder, min_photos)

    # save data as pickled object and as reader-friendly .txt file
    save_data(data, save_folder)

    # print reader friendly version of 'data' to screen
    pprint.pprint(data)

    print("\n*****Done*****\n")


if __name__ == "__main__":
    main()
\$\endgroup\$
  • 1
    \$\begingroup\$ Ok. I feel as if I'm simply repeating what is already in the comments at the top of the script. I understood this to be ok as unlike stack overflow, you may post only code here. \$\endgroup\$ – Michael Johnson Dec 8 '16 at 11:06
  • \$\begingroup\$ Apart from what @MathiasEttinger suggested, nice PEP8-compliant piece of code :) \$\endgroup\$ – Grajdeanu Alex. Dec 8 '16 at 11:14
  • \$\begingroup\$ +1 for teaching me that endswith takes a tuple to denote an or relationship. \$\endgroup\$ – Graipher Dec 8 '16 at 11:29
1
\$\begingroup\$

Your code is overall pretty clean!

I'd change the name of the method check_size to maybe filter_by_size. The reason behind this is that I'd expect a function that starts with check to return a boolean, not a list, which makes a line like this : photos = check_size(all_images, min_dimension) kind of weird. In the check_size method, there's another weird thing :

You check if the smallest dimension of your image is bigger than your min dimension. I feel like that's a weird requirement, but if that's what you want, I'd consider making it very clear that's what's happening in this method by adding a docstring to your method. Otherwise, if I have an image of 1000x100 and min_dim = 200, by reading the method's name I don't know what would happen.

In the photo_search method, you have this comment : get a list of all photos present in folder, but that's not what's happening in the line below. Which brings me to my point, you should only have comments when it's a certainty that someone reading your code wouldn't understand what the code's supposed to do, otherwise you end up having deprecated comments (that's the hard reality of development). To conclude this point, I think that all of your comments shouldn't be there, your code is clear as it is.

In the save_data method, I think you should have a method argument to decide what format to save your output into, because I'd be inclined to believe you most of the time don't use both of the outputs (but I might be wrong).

| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.