From 43e25870410eb46cc2f047a16759e39a71996d24 Mon Sep 17 00:00:00 2001 From: "Antonio J. Delgado" Date: Mon, 6 Sep 2021 15:52:20 +0300 Subject: [PATCH] Initial commit --- .gitignore | 143 +++++++++++++++++++++++++++ README.md | 1 + image_classifier/__init__.py | 0 image_classifier/image_classifier.py | 139 ++++++++++++++++++++++++++ pyproject.toml | 3 + requirements.txt | 4 + setup.py | 20 ++++ 7 files changed, 310 insertions(+) create mode 100755 .gitignore create mode 100755 README.md create mode 100755 image_classifier/__init__.py create mode 100755 image_classifier/image_classifier.py create mode 100755 pyproject.toml create mode 100755 requirements.txt create mode 100755 setup.py diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..ae138df --- /dev/null +++ b/.gitignore @@ -0,0 +1,143 @@ + Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# Configuration files +*.conf +*.cfg +*.ini \ No newline at end of file diff --git a/README.md b/README.md new file mode 100755 index 0000000..e18225e --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +== image_classifier diff --git a/image_classifier/__init__.py b/image_classifier/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/image_classifier/image_classifier.py b/image_classifier/image_classifier.py new file mode 100755 index 0000000..db8cbf7 --- /dev/null +++ b/image_classifier/image_classifier.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +# -*- encoding: utf-8 -*- +# +# This script is licensed under GNU GPL version 2.0 or above +# (c) 2021 Antonio J. Delgado +# + +import sys +import os +import logging +import json +import click +import click_config_file +from logging.handlers import SysLogHandler +import face_recognition +import exif + +class CustomFormatter(logging.Formatter): + """Logging colored formatter, adapted from https://stackoverflow.com/a/56944256/3638629""" + + grey = '\x1b[38;21m' + blue = '\x1b[38;5;39m' + yellow = '\x1b[38;5;226m' + red = '\x1b[38;5;196m' + bold_red = '\x1b[31;1m' + reset = '\x1b[0m' + + def __init__(self, fmt): + super().__init__() + self.fmt = fmt + self.FORMATS = { + logging.DEBUG: self.grey + self.fmt + self.reset, + logging.INFO: self.blue + self.fmt + self.reset, + logging.WARNING: self.yellow + self.fmt + self.reset, + logging.ERROR: self.red + self.fmt + self.reset, + logging.CRITICAL: self.bold_red + self.fmt + self.reset + } + + def format(self, record): + log_fmt = self.FORMATS.get(record.levelno) + formatter = logging.Formatter(log_fmt) + return formatter.format(record) + +class image_classifier: + + def __init__(self, debug_level, log_file, faces_folder, directory): + ''' Initial function called when object is created ''' + self.debug_level = debug_level + if log_file is None: + log_file = os.path.join(os.environ.get('HOME', os.environ.get('USERPROFILE', os.getcwd())), 'log', 'image_classifier.log') + self.log_file = log_file + self._init_log() + self.faces_folder = faces_folder + self.directory = directory + self.known_people = self.load_known_people() + + if os.access(directory, os.R_OK): + with os.scandir(directory) as directory_item: + for entry in directory_item: + if not entry.name.startswith('.') and entry.is_file(): + self.process_file(entry.name) + + def process_file(self, file): + ''' Process a file, find faces, add EXIF information and + move it to the folder of the day''' + people = self.find_faces(file) + with open(file, 'rb') as image_file: + exif_info = exif.Image(image_file) + if exif_info.has_exif: + print(json.dumps(exif_info, indent=2)) + # get date + # move to destination + + def load_known_people(self): + known_people = list() + if os.access(self.faces_folder, os.R_OK): + with os.scandir(self.faces_folder) as faces_items: + for entry in faces_items: + if not entry.name.startswith('.') and entry.is_file(): + person = dict() + person['filename'] = face_recognition.load_image_file(entry.name) + person['name'] = os.path.splitext(entry.name)[0] + person['encoding'] = face_recognition.face_encodings(person['filename'])[0] + known_people.append(person) + return known_people + + def find_faces(self, file): + ''' Find faces in an image/video file ''' + image = face_recognition.load_image_file(file) + encodings = face_recognition.face_encodings(image) + names = list() + for known_person in self.known_people: + if known_person['encoding'] in encodings: + names.append(known_person['name']) + return names + + def _init_log(self): + ''' Initialize log object ''' + self._log = logging.getLogger("image_classifier") + self._log.setLevel(logging.DEBUG) + + sysloghandler = SysLogHandler() + sysloghandler.setLevel(logging.DEBUG) + self._log.addHandler(sysloghandler) + + streamhandler = logging.StreamHandler(sys.stdout) + streamhandler.setLevel(logging.getLevelName(self.debug_level)) + #formatter = '%(asctime)s | %(levelname)8s | %(message)s' + formatter = '[%(levelname)s] %(message)s' + streamhandler.setFormatter(CustomFormatter(formatter)) + self._log.addHandler(streamhandler) + + if not os.path.exists(os.path.dirname(self.log_file)): + os.mkdir(os.path.dirname(self.log_file)) + + filehandler = logging.handlers.RotatingFileHandler(self.log_file, maxBytes=102400000) + # create formatter + formatter = logging.Formatter('%(asctime)s %(name)-12s %(levelname)-8s %(message)s') + filehandler.setFormatter(formatter) + filehandler.setLevel(logging.DEBUG) + self._log.addHandler(filehandler) + return True + +@click.command() +@click.option("--debug-level", "-d", default="INFO", + type=click.Choice( + ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG", "NOTSET"], + case_sensitive=False, + ), help='Set the debug level for the standard output.') +@click.option('--log-file', '-l', help="File to store all debug messages.") +@click.option("--faces-directory","-f", required=True, help="Folder containing the pictures that identify people. The filename would be used as the name for the person. Just one person per picture.") +@click.option("--directory","-d", required=True, help="Folder containing the pictures to classify.") +@click_config_file.configuration_option() +def __main__(debug_level, log_file, faces_folder, directory): + object = image_classifier(debug_level, log_file, faces_folder, directory) + +if __name__ == "__main__": + __main__() + diff --git a/pyproject.toml b/pyproject.toml new file mode 100755 index 0000000..9787c3b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/requirements.txt b/requirements.txt new file mode 100755 index 0000000..0b3eb9e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +click +click_config_file +face_recognition +exif \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..3f508f7 --- /dev/null +++ b/setup.py @@ -0,0 +1,20 @@ +import setuptools +import os + +requirements = list() +requirements_file = 'requirements.txt' +if os.access(requirements_file, os.R_OK): + with open(requirements_file, 'r') as requirements_file_pointer: + requirements = requirements_file_pointer.read().split() +setuptools.setup( + scripts=['image_classifier/image_classifier.py'], + author="Antonio J. Delgado", + version='0.0.1', + name='image_classifier', + author_email="", + url="", + description="", + license="GPLv3", + install_requires=requirements, + #keywords=["my", "script", "does", "things"] +)