Skip to content

Commit

Permalink
CLI working, opencv now >3
Browse files Browse the repository at this point in the history
  • Loading branch information
leblancfg committed Sep 27, 2017
1 parent af82327 commit ba48177
Show file tree
Hide file tree
Showing 56 changed files with 34,592 additions and 19 deletions.
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
include README.md LICENSE
include autocrop/*
79 changes: 79 additions & 0 deletions autocrop.egg-info/PKG-INFO
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
Metadata-Version: 1.1
Name: autocrop
Version: 0.1
Summary: Automatically crops faces from batches of pictures
Home-page: https://github.com/leblancfg/autocrop
Author: Francois Leblanc
Author-email: [email protected]
License: MIT
Description:
# autocrop

![obama_crop](https://cloud.githubusercontent.com/assets/15659410/10975709/3e38de48-83b6-11e5-8885-d95da758ca17.png)

Basic script using openCV, that automatically detects and crops faces from batches of photos.

Perfect for batch work for ID cards or profile pictures, will output 500px wide square images, centered around the biggest face detected. It can also add a touch of auto gamma correction.

## Installation
Simple!

The script will process all `.jpg` files in the `/photos` directory. The cropped files are placed in `photos/crop`, and originals are moved to `photos/bkp`.

If it can't find a face in the picture, it'll simply leave it in `/photos`.

### conda
The easiest way to run *autocrop* is to use the [Anaconda Python distribution](https://www.anaconda.com/download/) and run the following:

git clone https://github.com/leblancfg/autocrop
conda install --channel conda-forge --file requirements.txt

Move your pictures to be cropped in the *photos* directory, then run the script with:

cd autocrop
python autocrop.py

If running on Windows, this is by far the sanest way to approach this problem. Also, installing Anaconda doesn't require admin privileges on Windows.

### pip
Otherwise, binaries for the Python-only bindings for OpenCV have recently been made available through pip, which makes installation a breeze.

git clone https://github.com/leblancfg/autocrop
pip install numpy opencv-python

Move your pictures to be cropped in the *photos* directory, then run the script with:

cd autocrop
python autocrop.py

## Versions
The script works on openCV 2.4.9 and python 2.7+ and 3+. It has not been tested otherwise. For now, it also artificially restricts filetype as jpg and output size as 500px. These values can easily be tweaked in the header in `autocrop.py`.

## More Info
Check out:
* http://docs.opencv.org/master/d7/d8b/tutorial_py_face_detection.html#gsc.tab=0
* http://docs.opencv.org/master/d5/daf/tutorial_py_histogram_equalization.html#gsc.tab=0

Adapted from:
* http://photo.stackexchange.com/questions/60411/how-can-i-batch-crop-based-on-face-location

## TODO
Pull requests welcome! I don't see major feature additions in the future, but proper
* [ ] Create PyPI and conda-forge packages so that it can be directly pip- or conda-installable.
* [ ] Split off into smaller functions, and write unit tests.
* [ ] Handle input filetypes for `*.bmp`, `*.dib`, `*.jp2`, `*.png`, `*.webp`, `*.pbm`, `*.pgm`, `*.ppm`, `*.sr`, `*.ras`, `*.tiff`, `*.tif`.
* [ ] Handle output image size.
* [ ] Handle CLI input: `$ autocrop [-w width] [-h height] [-i input-folder] [-o output-folder] [--passport=<country>]`

Platform: UNKNOWN
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 2.6
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.3
Classifier: Programming Language :: Python :: 3.4
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
17 changes: 17 additions & 0 deletions autocrop.egg-info/SOURCES.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
LICENSE
MANIFEST.in
README.md
setup.cfg
setup.py
autocrop/.__init__.py.swp
autocrop/.autocrop.py.swp
autocrop/__init__.py
autocrop/__version__.py
autocrop/autocrop.py
autocrop/haarcascade_frontalface_default.xml
autocrop.egg-info/PKG-INFO
autocrop.egg-info/SOURCES.txt
autocrop.egg-info/dependency_links.txt
autocrop.egg-info/entry_points.txt
autocrop.egg-info/requires.txt
autocrop.egg-info/top_level.txt
1 change: 1 addition & 0 deletions autocrop.egg-info/dependency_links.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

3 changes: 3 additions & 0 deletions autocrop.egg-info/entry_points.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[console_scripts]
autocrop = autocrop:cli

2 changes: 2 additions & 0 deletions autocrop.egg-info/requires.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
numpy
opencv-python
1 change: 1 addition & 0 deletions autocrop.egg-info/top_level.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
autocrop
Binary file added autocrop/.__init__.py.swp
Binary file not shown.
Binary file added autocrop/.autocrop.py.swp
Binary file not shown.
2 changes: 2 additions & 0 deletions autocrop/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import argparse
import os
import sys

Expand All @@ -13,3 +14,4 @@

if __name__ == '__main__':
cli()

Binary file added autocrop/__pycache__/__init__.cpython-36.pyc
Binary file not shown.
Binary file added autocrop/__pycache__/autocrop.cpython-36.pyc
Binary file not shown.
3 changes: 3 additions & 0 deletions autocrop/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
__title__ = 'autocrop'
__description__ = 'Automatically crops faces from batches of pictures'
__author__ = 'François Leblanc'
__version__ = '0.1'
45 changes: 29 additions & 16 deletions autocrop/autocrop.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,30 @@
from __future__ import print_function

import argparse
from contextlib import contextmanager
import cv2
import glob
import numpy as np
import argparse
import os
import shutil
import sys

from .__version__ import __title__, __description__, __author__, __version__


# Internal variables
errors = 0
fixexp = True # Flag to fix underexposition
marker = False # Flag for gamma correct
INPUT_FILETYPES = ['*.jpg', '*.jpeg']
INCREMENT = 0.06
GAMMA_THRES = 0.001
GAMMA = 0.90
FACE_RATIO = 6
cascPath = 'haarcascade_frontalface_default.xml'

# Load XML Resource
cascFile= 'haarcascade_frontalface_default.xml'
d = os.path.dirname(sys.modules['autocrop'].__file__)
cascPath = os.path.join(d, cascFile)

# Define directory change within context
@contextmanager
Expand All @@ -34,11 +41,16 @@ def gamma(img, correction):
img = cv2.pow(img/255.0, correction)
return np.uint8(img*255)

def main():
def main(path, fheight, fwidth):
"""Given path containing image files to process, will
1) copy them to `path/bkp`, and
2) create face-cropped versions and place them in `path/crop`
"""
errors = 0
# Create the haar cascade
faceCascade = cv2.CascadeClassifier(cascPath)

with cd('../photos/'):
with cd(path):
files_grabbed = []
for files in INPUT_FILETYPES:
files_grabbed.extend(glob.glob(files))
Expand All @@ -52,13 +64,12 @@ def main():
minface = int(np.sqrt(height*height + width*width) / 8)

# ====== Detect faces in the image ======
faces = [[]]
faces = faceCascade.detectMultiScale(
gray,
scaleFactor=1.1,
minNeighbors=5,
minSize=(minface, minface),
flags = cv2.cv.CV_HAAR_FIND_BIGGEST_OBJECT | cv2.cv.CV_HAAR_DO_ROUGH_SEARCH
flags = cv2.CASCADE_FIND_BIGGEST_OBJECT | cv2.CASCADE_DO_ROUGH_SEARCH
)

# Handle no faces
Expand All @@ -82,7 +93,11 @@ def main():
break

# Crop the image from the original
image = image[y-2*pad:y+h+pad, x-1.5*pad:x+w+1.5*pad]
h1 = int(x-1.5*pad)
h2 = int(x+w+1.5*pad)
v1 = int(y-2*pad)
v2 = int(y+h+pad)
image = image[v1:v2, h1:h2]

# Resize the damn thing
image = cv2.resize(image, (fheight, fwidth), interpolation = cv2.INTER_AREA)
Expand All @@ -108,14 +123,12 @@ def main():

def cli():
parser = argparse.ArgumentParser(description='Automatically crops faces from batches of pictures')
parser.add_argument('-w', '--width', default=500, help='Width of the cropped files in pixels')
parser.add_argument('-h', '--height', default=500, help='Height of the cropped files in pixels')
args = parser.parse_args()
fwidth = args.width
fheight = args.height
parser.add_argument('-p', '--path', default='photos', help='Path where images to crop are located')
parser.add_argument('-w', '--width', type=int, default=500, help='Width of the cropped files in pixels')
parser.add_argument('-H', '--height', type=int, default=500, help='Height of the cropped files in pixels')

# if len(args) != 1:
# parser.error('wrong number of arguments')
args = parser.parse_args()
print('Processing images in folder:', path)

main()
main(args.path, args.height, args.width)

Binary file added build/lib/autocrop/.__init__.py.swp
Binary file not shown.
Binary file added build/lib/autocrop/.autocrop.py.swo
Binary file not shown.
Binary file added build/lib/autocrop/.autocrop.py.swp
Binary file not shown.
17 changes: 17 additions & 0 deletions build/lib/autocrop/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import argparse
import os
import sys

# Inject vendored directory into system path.
v_path = os.path.abspath(os.path.sep.join([os.path.dirname(os.path.realpath(__file__)), 'vendor']))
sys.path.insert(0, v_path)

# Inject patched directory into system path.
v_path = os.path.abspath(os.path.sep.join([os.path.dirname(os.path.realpath(__file__)), 'patched']))
sys.path.insert(0, v_path)

from .autocrop import cli

if __name__ == '__main__':
cli()

4 changes: 4 additions & 0 deletions build/lib/autocrop/__version__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
__title__ = 'autocrop'
__description__ = 'Automatically crops faces from batches of pictures'
__author__ = 'François Leblanc'
__version__ = '0.1'
134 changes: 134 additions & 0 deletions build/lib/autocrop/autocrop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
from __future__ import print_function

import argparse
from contextlib import contextmanager
import cv2
import glob
import numpy as np
import os
import shutil
import sys

from .__version__ import __title__, __description__, __author__, __version__


# Internal variables
fixexp = True # Flag to fix underexposition
marker = False # Flag for gamma correct
INPUT_FILETYPES = ['*.jpg', '*.jpeg']
INCREMENT = 0.06
GAMMA_THRES = 0.001
GAMMA = 0.90
FACE_RATIO = 6

# Load XML Resource
cascFile= 'haarcascade_frontalface_default.xml'
d = os.path.dirname(sys.modules['autocrop'].__file__)
cascPath = os.path.join(d, cascFile)

# Define directory change within context
@contextmanager
def cd(newdir):
prevdir = os.getcwd()
os.chdir(os.path.expanduser(newdir))
try:
yield
finally:
os.chdir(prevdir)

# Define simple gamma correction fn
def gamma(img, correction):
img = cv2.pow(img/255.0, correction)
return np.uint8(img*255)

def main(path, fheight, fwidth):
"""Given path containing image files to process, will
1) copy them to `path/bkp`, and
2) create face-cropped versions and place them in `path/crop`
"""
errors = 0
# Create the haar cascade
faceCascade = cv2.CascadeClassifier(cascPath)

with cd(path):
files_grabbed = []
for files in INPUT_FILETYPES:
files_grabbed.extend(glob.glob(files))

for file in files_grabbed:
image = cv2.imread(file)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

# Scale the image
height, width = (image.shape[:2])
minface = int(np.sqrt(height*height + width*width) / 8)

# ====== Detect faces in the image ======
faces = faceCascade.detectMultiScale(
gray,
scaleFactor=1.1,
minNeighbors=5,
minSize=(minface, minface),
flags = cv2.CASCADE_FIND_BIGGEST_OBJECT | cv2.CASCADE_DO_ROUGH_SEARCH
)

# Handle no faces
if len(faces) == 0:
print(' No faces can be detected in file {0}.'.format(str(file)))
errors += 1
break

# Copy to /bkp
shutil.copy(file, 'bkp')

# Make padding from probable biggest face
x, y, w, h = faces[-1]
pad = h / FACE_RATIO

# Make sure padding is contained within picture
while True: # decreases pad by 6% increments to fit crop into image. Can lead to very small faces.
if y-2*pad < 0 or y+h+pad > height or int(x-1.5*pad) < 0 or x+w+int(1.5*pad) > width:
pad = (1 - INCREMENT) * pad
else:
break

# Crop the image from the original
h1 = int(x-1.5*pad)
h2 = int(x+w+1.5*pad)
v1 = int(y-2*pad)
v2 = int(y+h+pad)
image = image[v1:v2, h1:h2]

# Resize the damn thing
image = cv2.resize(image, (fheight, fwidth), interpolation = cv2.INTER_AREA)

# ====== Dealing with underexposition ======
if fixexp == True:
# Check if under-exposed
uexp = cv2.calcHist([gray], [0], None, [256], [0,256])

if sum(uexp[-26:]) < GAMMA_THRES * sum(uexp) :
marker = True
image = gamma(image, GAMMA)

# Write cropfile
cropfilename = '{0}'.format(str(file))
cv2.imwrite(cropfilename, image)

# Move files to /crop
shutil.move(cropfilename, 'crop')

# Stop and print timer
print(' {0} files have been cropped'.format(len(files_grabbed) - errors))

def cli():
parser = argparse.ArgumentParser(description='Automatically crops faces from batches of pictures')
parser.add_argument('-p', '--path', default='photos', help='Path where images to crop are located')
parser.add_argument('-w', '--width', type=int, default=500, help='Width of the cropped files in pixels')
parser.add_argument('-H', '--height', type=int, default=500, help='Height of the cropped files in pixels')

args = parser.parse_args()
print('Processing images in folder:', path)

main(args.path, args.height, args.width)

Loading

0 comments on commit ba48177

Please sign in to comment.