7
\$\begingroup\$

I made this Python 3.6 program that generates a list of images that fail to compare against a given ratio. Each image has a resolution and with a bit of math, the program is supposed to calculate the width using the ratio and compare it with the actual width. If the calculated width falls from the range of (width-1, width+1), the image is outputted and considered "not respecting the ratio".

Important: The program uses the PIL library to get the resolutions. I used pip --user and it seems to work perfectly.

For example, an image with the resolution of 1920x1080 is in a directory. The program, imageres.py, takes the resolution, and uses this formula:

$$\text{cal_width} = \frac{\text{width_ratio} * \text{image_height}}{\text{height_ratio}}$$

If the ratio (default, actually) is 16:9, the formula turns out to be this:

$$\text{cal_width} = \frac{16 * 1080}{9}$$

Thus, the answer should be:

$$\text{cal_width} = 1920$$

The program does the above and compares the final result to the image's actual resolution. Because both of them are equal, the image is not outputted and moves on to the next one.


Do you have any suggestions or methods I can use to make my code more robust? I noticed PyLint kept telling me on structure, so if anyone has any suggestion to make the code more readable would be awesome.

My biggest concern, though, is the mathematics and floating-point numbers behind the program. If you understand what I'm trying to do, I would like to hear your opinions! I have some reservations about this, as it seems more of a hack than an actual solution.

imageres.py

(I hope I was able to keep the PyLint stuff on.)

"""
    imgres.py

    this is a simple python image resoultion ratio checker.  quite loaded
    sentence, but basically it gets an image's width and height and calculates
    the width using the hardcoded default ratio of 16:9, which is what some
    monitors have (like 1920x1080 is a equal ratio to 16:9)

    so far it only spits out the images that are not equal to the width,
    however it is quite possible that calculations are a pixel above/below the
    image width, due to precison error.
    ^- not exactly solved, but it checks by an offset of 1

    requires the PIL (Python Image Library) locally via pip or else where
"""
from os import listdir
from os.path import isfile, join
from argparse import ArgumentParser
from math import trunc

import sys

from PIL import Image

# the parser so we can present a user-friendly interface
PARSER = ArgumentParser(description='Compare image resolutions to ratio.')
PARSER.add_argument('DIRECTORY', metavar='D', type=str, help="The folder where the images reside")
PARSER.add_argument('--wr', default=16, metavar='W', type=int, help="The width ratio. DEFAULT: 16")
PARSER.add_argument('--hr', default=9, metavar='H', type=int, help="The height ratio. DEFAULT: 9")

ARGS = PARSER.parse_args()

# debug; check that the args work
# print("%s %d %d" % (ARGS.DIRECTORY, ARGS.wr, ARGS.hr))

# validate directory is real and accessible
# found the error code list from here:
# http://www-numi.fnal.gov/offline_software/srt_public_context/WebDocs/Errors/unix_system_errors.html
try:
    ONLYFILES = [
        f for f in listdir(ARGS.DIRECTORY)
        if isfile(join(ARGS.DIRECTORY, f)) and f.endswith(('jpg', 'png'))
    ]

    # get only the files from the directory. does not transverse into
    # sub-directories. uses a tuple to get only jpg and png extensioned files
    for img_filepath in ONLYFILES:
        img = Image.open(join(ARGS.DIRECTORY, img_filepath))
        # debug; check it's possible to get the image resolution
        # print(img.size)
        img_width, img_height = img.size

        # calculate the width of the image with the provided ratio
        calculated_width = trunc((ARGS.wr * img_height) / float(ARGS.hr))

        # check if the calculated_width falls between the range with +- offset
        # if yes, output the filename with resolution and calculated_width
        if not calculated_width in range(img_width-1, img_width+1):
            print("%30s -- (%d,%d) ~ C_W %f" %
                  (img_filepath, img_width, img_height, calculated_width)
                 )
except FileNotFoundError as inst:
    print("error: the specified argument does not exist.\n", inst)
    sys.exit(2)
except NotADirectoryError as inst:
    print("error: the specified argument is not an directory.\n", inst)
    sys.exit(20)

Here is an image of it working

\$\endgroup\$
4
\$\begingroup\$

Here are some of the notes about the code you've posted:

  • I think pillow project is a much more active player than PIL, consider switching. There is also a highly-optimized Pillow-SIMD that can bring dramatically better performance (some benchmarks).
  • variable naming - PARSER, ARGS, ONLYFILES are not constants (well, there are no constants in Python, it's just that it is recommended to name things that don't change in an upper case, PEP8 about constants) and should be defined in a lower case
  • you don't need to define ONLY_FILES list and then iterate over it, you can use a single loop and, moreover, use a glob pattern with glob.iglob():

    import glob 
    
    for filename in glob.iglob(args.directory + "/*.{jpg,png}", recursive=True):
    
  • when opening the image files, use the with context manager:

    with Image.open(filename) as img:
    
  • when you check the calculated_width to be in a specified range, use comparison operators instead of creating a extra "range" (please check the following on the off-by-one errors):

    if not(img_width - 1 <= calculated_width <= img_width):
    
  • you can have a custom argparse directory type

  • I don't see much sense in keeping a filename inside a "docstring" - the filename can change and you would easily forget to update it in the docstring
  • the "requires the PIL (Python Image Library) locally via pip or else where" should be better handled by requirements.txt file accompanied by a "README" file (typically README.md or README.rst) with installation, usage and license instructions
  • it might probably be a good idea to configure the --help of your CLI program, argparse would by default generate one, but adding more to the usage instructions and potential problems may be helpful
  • the commented "debug" section of the code, should be replaced by a proper logging
  • organize imports as per PEP8 recommendations
| improve this answer | |
\$\endgroup\$
0
\$\begingroup\$

I am focusing on your question about the mathematics. In python 3.x, it is useless to apply float() to ARGS.hr since /``does not match floor division any more (//` has been introduced) but real division.

I suppose your main concern is about trunc(). I don't know what is better. I would have used round() instead because I expect image converting softwares to handle the problem that way when performing a resize. However, I did not check and you can bet different softwares apply different policies... Did you try to shrink a 1920x1080 pic of, say, 30% to see what rule was used ?

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