Module autocrop.cli
None
None
View Source
import argparse
import os
import shutil
import sys
from typing import Optional
from PIL import Image
from .__version__ import __version__
from .autocrop import Cropper, ImageReadError
from .constants import (
QUESTION_OVERWRITE,
CV2_FILETYPES,
PILLOW_FILETYPES,
)
COMBINED_FILETYPES = CV2_FILETYPES + PILLOW_FILETYPES
INPUT_FILETYPES = COMBINED_FILETYPES + [s.upper() for s in COMBINED_FILETYPES]
def output(input_filename, output_filename, image):
"""
Move the input file to the output location and write over it with the
cropped image data.
"""
if input_filename != output_filename:
# Move the file to the output directory
shutil.copy(input_filename, output_filename)
# Encode the image as an in-memory PNG
img_new = Image.fromarray(image)
# Write the new image (converting the format to match the output
# filename if necessary)
img_new.save(output_filename)
def reject(input_filename, reject_filename):
"""Move the input file to the reject location."""
if input_filename != reject_filename:
# Move the file to the reject directory
shutil.copy(input_filename, reject_filename)
def main(
input_d: str,
output_d: str,
reject_d: str,
extension: Optional[str] = None,
fheight: int = 500,
fwidth: int = 500,
facePercent: int = 50,
resize: bool = True,
) -> None:
"""
Crops folder of images to the desired height and width if a
face is found.
If `input_d == output_d` or `output_d is None`, overwrites all files
where the biggest face was found.
Parameters:
-----------
- `input_d`: `str`
* Directory to crop images from.
- `output_d`: `str`
* Directory where cropped images are placed.
- `reject_d`: `str`
* Directory where images that cannot be cropped are placed.
- `fheight`: `int`, default=`500`
* Height (px) to which to crop the image.
- `fwidth`: `int`, default=`500`
* Width (px) to which to crop the image.
- `facePercent`: `int`, default=`50`
* Percentage of face from height.
- `extension` : `str`
* Image extension to save at output.
- `resize`: `bool`, default=`True`
* If `False`, don't resize the image, but use the original size.
Side Effects:
-------------
- Creates image files in output directory.
"""
reject_count = 0
output_count = 0
input_files = [
os.path.join(input_d, f)
for f in os.listdir(input_d)
if any(f.endswith(t) for t in INPUT_FILETYPES)
]
if output_d is None:
output_d = input_d
if reject_d is None and output_d is None:
reject_d = input_d
if reject_d is None:
reject_d = output_d
# Guard against calling the function directly
input_count = len(input_files)
assert input_count > 0
# Main loop
cropper = Cropper(
width=fwidth, height=fheight, face_percent=facePercent, resize=resize
)
for input_filename in input_files:
basename = os.path.basename(input_filename)
if extension:
basename_noext = os.path.splitext(basename)[0]
output_filename = os.path.join(output_d, basename_noext + "." + extension)
else:
output_filename = os.path.join(output_d, basename)
reject_filename = os.path.join(reject_d, basename)
image = None
# Attempt the crop
try:
image = cropper.crop(input_filename)
except ImageReadError:
print("Read error: {}".format(input_filename))
continue
# Did the crop produce an invalid image?
if isinstance(image, type(None)):
reject(input_filename, reject_filename)
print("No face detected: {}".format(reject_filename))
reject_count += 1
else:
output(input_filename, output_filename, image)
print("Face detected: {}".format(output_filename))
output_count += 1
# Stop and print status
print(
f"{input_count} : Input files, {output_count} : Faces Cropped, {reject_count}"
)
def input_path(p):
"""Returns path, only if input is a valid directory."""
no_folder = "Input folder does not exist"
no_images = "Input folder does not contain any image files"
p = os.path.abspath(p)
if not os.path.isdir(p):
raise argparse.ArgumentTypeError(no_folder)
filetypes = {os.path.splitext(f)[-1] for f in os.listdir(p)}
if not any(t in INPUT_FILETYPES for t in filetypes):
raise argparse.ArgumentTypeError(no_images)
else:
return p
def output_path(p):
"""
Returns path, if input is a valid directory name.
If directory doesn't exist, creates it.
"""
p = os.path.abspath(p)
if not os.path.isdir(p):
os.makedirs(p)
return p
def size(i):
"""Returns valid only if input is a positive integer under 1e5"""
error = "Invalid pixel size"
try:
i = int(i)
except ValueError:
raise argparse.ArgumentTypeError(error)
if i > 0 and i < 1e5:
return i
else:
raise argparse.ArgumentTypeError(error)
def compat_input(s=""): # pragma: no cover
"""Compatibility function to permit testing for Python 2 and 3."""
try:
return raw_input(s)
except NameError:
# Py2 raw_input() renamed to input() in Py3
return input(s) # lgtm[py/use-of-input]
def confirmation(question):
"""Ask a yes/no question via standard input and return the answer."""
yes_list = ["yes", "y"]
no_list = ["no", "n"]
default_str = "[Y]/n"
prompt_str = "{} {} ".format(question, default_str)
while True:
choice = compat_input(prompt_str).lower()
if not choice:
return default_str
if choice in yes_list:
return True
if choice in no_list:
return False
notification_str = "Please respond with 'y' or 'n'"
print(notification_str)
def chk_extension(extension):
"""Check if the extension passed is valid or not."""
error = "Invalid image extension"
extension = str(extension).lower()
if not extension.startswith("."):
extension = f".{extension}"
if extension in COMBINED_FILETYPES:
return extension.lower().replace(".", "")
else:
raise argparse.ArgumentTypeError(error)
def parse_args(args):
"""Helper function. Parses the arguments given to the CLI."""
help_d = {
"desc": "Automatically crops faces from batches of pictures",
"input": """Folder where images to crop are located. Default:
current working directory""",
"output": """Folder where cropped images will be moved to.
Default: current working directory, meaning images are
cropped in place.""",
"reject": """Folder where images that could not be cropped will be
moved to.
Default: current working directory, meaning images that
are not cropped will be left in place.""",
"extension": "Enter the image extension which to save at output",
"width": "Width of cropped files in px. Default=500",
"height": "Height of cropped files in px. Default=500",
"y": "Bypass any confirmation prompts",
"facePercent": "Percentage of face to image height",
"no_resize": """Do not resize images to the specified width and height,
but instead use the original image's pixels.""",
}
parser = argparse.ArgumentParser(description=help_d["desc"])
parser.add_argument(
"-v",
"--version",
action="version",
version="%(prog)s version {}".format(__version__),
)
parser.add_argument("--no-confirm", action="store_true", help=help_d["y"])
parser.add_argument(
"-n",
"--no-resize",
action="store_true",
help=help_d["no_resize"],
)
parser.add_argument(
"-i", "--input", default=".", type=input_path, help=help_d["input"]
)
parser.add_argument(
"-o",
"--output",
"-p",
"--path",
type=output_path,
default=None,
help=help_d["output"],
)
parser.add_argument(
"-r", "--reject", type=output_path, default=None, help=help_d["reject"]
)
parser.add_argument("-w", "--width", type=size, default=500, help=help_d["width"])
parser.add_argument("-H", "--height", type=size, default=500, help=help_d["height"])
parser.add_argument(
"--facePercent", type=size, default=50, help=help_d["facePercent"]
)
parser.add_argument(
"-e", "--extension", type=chk_extension, default=None, help=help_d["extension"]
)
return parser.parse_args()
def command_line_interface():
"""
AUTOCROP
--------
Crops faces from batches of images.
"""
args = parse_args(sys.argv[1:])
if not args.no_confirm:
if args.output is None or args.input == args.output:
if not confirmation(QUESTION_OVERWRITE):
sys.exit()
if args.input == args.output:
args.output = None
print("Processing images in folder:", args.input)
resize = not args.no_resize
main(
args.input,
args.output,
args.reject,
args.extension,
args.height,
args.width,
args.facePercent,
resize,
)
Variables
COMBINED_FILETYPES
CV2_FILETYPES
INPUT_FILETYPES
PILLOW_FILETYPES
QUESTION_OVERWRITE
Functions
chk_extension
def chk_extension(
extension
)
Check if the extension passed is valid or not.
View Source
def chk_extension(extension):
"""Check if the extension passed is valid or not."""
error = "Invalid image extension"
extension = str(extension).lower()
if not extension.startswith("."):
extension = f".{extension}"
if extension in COMBINED_FILETYPES:
return extension.lower().replace(".", "")
else:
raise argparse.ArgumentTypeError(error)
command_line_interface
def command_line_interface(
)
AUTOCROP
Crops faces from batches of images.
View Source
def command_line_interface():
"""
AUTOCROP
--------
Crops faces from batches of images.
"""
args = parse_args(sys.argv[1:])
if not args.no_confirm:
if args.output is None or args.input == args.output:
if not confirmation(QUESTION_OVERWRITE):
sys.exit()
if args.input == args.output:
args.output = None
print("Processing images in folder:", args.input)
resize = not args.no_resize
main(
args.input,
args.output,
args.reject,
args.extension,
args.height,
args.width,
args.facePercent,
resize,
)
compat_input
def compat_input(
s=''
)
Compatibility function to permit testing for Python 2 and 3.
View Source
def compat_input(s=""): # pragma: no cover
"""Compatibility function to permit testing for Python 2 and 3."""
try:
return raw_input(s)
except NameError:
# Py2 raw_input() renamed to input() in Py3
return input(s) # lgtm[py/use-of-input]
confirmation
def confirmation(
question
)
Ask a yes/no question via standard input and return the answer.
View Source
def confirmation(question):
"""Ask a yes/no question via standard input and return the answer."""
yes_list = ["yes", "y"]
no_list = ["no", "n"]
default_str = "[Y]/n"
prompt_str = "{} {} ".format(question, default_str)
while True:
choice = compat_input(prompt_str).lower()
if not choice:
return default_str
if choice in yes_list:
return True
if choice in no_list:
return False
notification_str = "Please respond with 'y' or 'n'"
print(notification_str)
input_path
def input_path(
p
)
Returns path, only if input is a valid directory.
View Source
def input_path(p):
"""Returns path, only if input is a valid directory."""
no_folder = "Input folder does not exist"
no_images = "Input folder does not contain any image files"
p = os.path.abspath(p)
if not os.path.isdir(p):
raise argparse.ArgumentTypeError(no_folder)
filetypes = {os.path.splitext(f)[-1] for f in os.listdir(p)}
if not any(t in INPUT_FILETYPES for t in filetypes):
raise argparse.ArgumentTypeError(no_images)
else:
return p
main
def main(
input_d: str,
output_d: str,
reject_d: str,
extension: Optional[str] = None,
fheight: int = 500,
fwidth: int = 500,
facePercent: int = 50,
resize: bool = True
) -> None
Crops folder of images to the desired height and width if a
face is found.
If input_d == output_d
or output_d is None
, overwrites all files
where the biggest face was found.
Parameters:
input_d
:str
- Directory to crop images from.
output_d
:str
- Directory where cropped images are placed.
reject_d
:str
- Directory where images that cannot be cropped are placed.
fheight
:int
, default=500
- Height (px) to which to crop the image.
fwidth
:int
, default=500
- Width (px) to which to crop the image.
facePercent
:int
, default=50
- Percentage of face from height.
extension
:str
- Image extension to save at output.
resize
:bool
, default=True
- If
False
, don't resize the image, but use the original size.
- If
Side Effects:
- Creates image files in output directory.
View Source
def main(
input_d: str,
output_d: str,
reject_d: str,
extension: Optional[str] = None,
fheight: int = 500,
fwidth: int = 500,
facePercent: int = 50,
resize: bool = True,
) -> None:
"""
Crops folder of images to the desired height and width if a
face is found.
If `input_d == output_d` or `output_d is None`, overwrites all files
where the biggest face was found.
Parameters:
-----------
- `input_d`: `str`
* Directory to crop images from.
- `output_d`: `str`
* Directory where cropped images are placed.
- `reject_d`: `str`
* Directory where images that cannot be cropped are placed.
- `fheight`: `int`, default=`500`
* Height (px) to which to crop the image.
- `fwidth`: `int`, default=`500`
* Width (px) to which to crop the image.
- `facePercent`: `int`, default=`50`
* Percentage of face from height.
- `extension` : `str`
* Image extension to save at output.
- `resize`: `bool`, default=`True`
* If `False`, don't resize the image, but use the original size.
Side Effects:
-------------
- Creates image files in output directory.
"""
reject_count = 0
output_count = 0
input_files = [
os.path.join(input_d, f)
for f in os.listdir(input_d)
if any(f.endswith(t) for t in INPUT_FILETYPES)
]
if output_d is None:
output_d = input_d
if reject_d is None and output_d is None:
reject_d = input_d
if reject_d is None:
reject_d = output_d
# Guard against calling the function directly
input_count = len(input_files)
assert input_count > 0
# Main loop
cropper = Cropper(
width=fwidth, height=fheight, face_percent=facePercent, resize=resize
)
for input_filename in input_files:
basename = os.path.basename(input_filename)
if extension:
basename_noext = os.path.splitext(basename)[0]
output_filename = os.path.join(output_d, basename_noext + "." + extension)
else:
output_filename = os.path.join(output_d, basename)
reject_filename = os.path.join(reject_d, basename)
image = None
# Attempt the crop
try:
image = cropper.crop(input_filename)
except ImageReadError:
print("Read error: {}".format(input_filename))
continue
# Did the crop produce an invalid image?
if isinstance(image, type(None)):
reject(input_filename, reject_filename)
print("No face detected: {}".format(reject_filename))
reject_count += 1
else:
output(input_filename, output_filename, image)
print("Face detected: {}".format(output_filename))
output_count += 1
# Stop and print status
print(
f"{input_count} : Input files, {output_count} : Faces Cropped, {reject_count}"
)
output
def output(
input_filename,
output_filename,
image
)
Move the input file to the output location and write over it with the
cropped image data.
View Source
def output(input_filename, output_filename, image):
"""
Move the input file to the output location and write over it with the
cropped image data.
"""
if input_filename != output_filename:
# Move the file to the output directory
shutil.copy(input_filename, output_filename)
# Encode the image as an in-memory PNG
img_new = Image.fromarray(image)
# Write the new image (converting the format to match the output
# filename if necessary)
img_new.save(output_filename)
output_path
def output_path(
p
)
Returns path, if input is a valid directory name.
If directory doesn't exist, creates it.
View Source
def output_path(p):
"""
Returns path, if input is a valid directory name.
If directory doesn't exist, creates it.
"""
p = os.path.abspath(p)
if not os.path.isdir(p):
os.makedirs(p)
return p
parse_args
def parse_args(
args
)
Helper function. Parses the arguments given to the CLI.
View Source
def parse_args(args):
"""Helper function. Parses the arguments given to the CLI."""
help_d = {
"desc": "Automatically crops faces from batches of pictures",
"input": """Folder where images to crop are located. Default:
current working directory""",
"output": """Folder where cropped images will be moved to.
Default: current working directory, meaning images are
cropped in place.""",
"reject": """Folder where images that could not be cropped will be
moved to.
Default: current working directory, meaning images that
are not cropped will be left in place.""",
"extension": "Enter the image extension which to save at output",
"width": "Width of cropped files in px. Default=500",
"height": "Height of cropped files in px. Default=500",
"y": "Bypass any confirmation prompts",
"facePercent": "Percentage of face to image height",
"no_resize": """Do not resize images to the specified width and height,
but instead use the original image's pixels.""",
}
parser = argparse.ArgumentParser(description=help_d["desc"])
parser.add_argument(
"-v",
"--version",
action="version",
version="%(prog)s version {}".format(__version__),
)
parser.add_argument("--no-confirm", action="store_true", help=help_d["y"])
parser.add_argument(
"-n",
"--no-resize",
action="store_true",
help=help_d["no_resize"],
)
parser.add_argument(
"-i", "--input", default=".", type=input_path, help=help_d["input"]
)
parser.add_argument(
"-o",
"--output",
"-p",
"--path",
type=output_path,
default=None,
help=help_d["output"],
)
parser.add_argument(
"-r", "--reject", type=output_path, default=None, help=help_d["reject"]
)
parser.add_argument("-w", "--width", type=size, default=500, help=help_d["width"])
parser.add_argument("-H", "--height", type=size, default=500, help=help_d["height"])
parser.add_argument(
"--facePercent", type=size, default=50, help=help_d["facePercent"]
)
parser.add_argument(
"-e", "--extension", type=chk_extension, default=None, help=help_d["extension"]
)
return parser.parse_args()
reject
def reject(
input_filename,
reject_filename
)
Move the input file to the reject location.
View Source
def reject(input_filename, reject_filename):
"""Move the input file to the reject location."""
if input_filename != reject_filename:
# Move the file to the reject directory
shutil.copy(input_filename, reject_filename)
size
def size(
i
)
Returns valid only if input is a positive integer under 1e5
View Source
def size(i):
"""Returns valid only if input is a positive integer under 1e5"""
error = "Invalid pixel size"
try:
i = int(i)
except ValueError:
raise argparse.ArgumentTypeError(error)
if i > 0 and i < 1e5:
return i
else:
raise argparse.ArgumentTypeError(error)