diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9d91ce6..2fb66e1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,11 +5,11 @@ repos: - id: nbstripout files: ".ipynb" - repo: https://github.com/psf/black - rev: 21.11b0 + rev: 22.6.0 hooks: - id: black - repo: https://github.com/dfm/black_nbconvert - rev: v0.3.0 + rev: v0.4.0 hooks: - id: black_nbconvert - repo: https://github.com/pycqa/flake8 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..23d2635 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +FROM jupyter/base-notebook:2022-06-06 + +# Install system dependencies for computer vision packages +USER root +RUN apt update && apt install -y libgl1 libglib2.0-0 libsm6 libxrender1 libxext6 +USER $NB_USER + +# Copy the repository into the container +COPY --chown=${NB_UID} . /opt/misinformation + +# Install the Python package +RUN python -m pip install /opt/misinformation + +# Make JupyterLab the default for this application +ENV JUPYTER_ENABLE_LAB=yes + +# Export where the data is located +ENV XDG_DATA_HOME=/opt/misinformation/data + +# Copy notebooks into the home directory +RUN rm -rf $HOME/work +RUN cp /opt/misinformation/notebooks/*.ipynb $HOME + +# Execute the facial recognition notebook once in order to bundle +# the pre-built models (that are downloaded on demand) into the +# Docker image. +RUN python -m pip install nbclick && \ + python -m nbclick /opt/misinformation/notebooks/facial_expressions.ipynb diff --git a/data/.keep b/data/.keep new file mode 100644 index 0000000..e69de29 diff --git a/misinformation/__init__.py b/misinformation/__init__.py new file mode 100644 index 0000000..9c571a3 --- /dev/null +++ b/misinformation/__init__.py @@ -0,0 +1,8 @@ +from importlib import metadata + + +# Export the version defined in project metadata +__version__ = metadata.version(__package__) +del metadata + +from misinformation.faces import explore_face_recognition, find_files diff --git a/misinformation/faces.py b/misinformation/faces.py new file mode 100644 index 0000000..16e174b --- /dev/null +++ b/misinformation/faces.py @@ -0,0 +1,89 @@ +import glob +import ipywidgets +import os + +from IPython.display import display +from deepface import DeepFace + + +def find_files(path=None, pattern="*.png", recursive=True, limit=20): + """Find image files on the file system + + :param path: + The base directory where we are looking for the images. Defaults + to None, which uses the XDG data directory if set or the current + working directory otherwise. + :param pattern: + The naming pattern that the filename should match. Defaults to + "*.png". Can be used to allow other patterns or to only include + specific prefixes or suffixes. + :param recursive: + Whether to recurse into subdirectories. + :param limit: + The maximum number of images to be found. Defaults to 20. + To return all images, set to None. + """ + if path is None: + path = os.environ.get("XDG_DATA_HOME", ".") + + result = list(glob.glob(f"{path}/{pattern}", recursive=recursive)) + + if limit is not None: + result = result[:limit] + + return result + + +def facial_expression_analysis(img_path): + return DeepFace.analyze( + img_path=img_path, actions=["age", "gender", "race", "emotion"], prog_bar=False + ) + + +class JSONContainer: + """Expose a Python dictionary as a JSON document in JupyterLab + rich display rendering. + """ + + def __init__(self, data={}): + self._data = data + + def _repr_json_(self): + return self._data + + +def explore_face_recognition(image_paths): + # Set up the facial recognition output widget + output = ipywidgets.Output(layout=ipywidgets.Layout(width="30%")) + + # Set up the image selection and display widget + images = [ipywidgets.Image.from_file(p) for p in image_paths] + image_widget = ipywidgets.Tab( + children=images, + titles=[f"#{i}" for i in range(len(image_paths))], + layout=ipywidgets.Layout(width="70%"), + ) + + # Register the facial recognition logic + def _recognition(_): + data = {} + data["filename"] = image_paths[image_widget.selected_index] + + try: + data["deepface_results"] = facial_expression_analysis(data["filename"]) + data["deepface_find_face"] = True + except ValueError: + data["deepface_find_face"] = False + + output.clear_output() + with output: + display(JSONContainer(data)) + + # Register the handler and trigger it immediately + image_widget.observe(_recognition, names=("selected_index",), type="change") + + with ipywidgets.Output(): + _recognition(None) + + # Show the combined widget + return ipywidgets.HBox([image_widget, output]) diff --git a/notebooks/facial_expressions.ipynb b/notebooks/facial_expressions.ipynb new file mode 100644 index 0000000..1a63a7e --- /dev/null +++ b/notebooks/facial_expressions.ipynb @@ -0,0 +1,105 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d2c4d40d-8aca-4024-8d19-a65c4efe825d", + "metadata": {}, + "source": [ + "# Facial Expression recognition with DeepFace" + ] + }, + { + "cell_type": "markdown", + "id": "51f8888b-d1a3-4b85-a596-95c0993fa192", + "metadata": {}, + "source": [ + "This notebooks shows some preliminary work on detecting facial expressions with DeepFace. It is mainly meant to explore its capabilities and to decide on future research directions. We package our code into a `misinformation` package that is imported here:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b21e52a5-d379-42db-aae6-f2ab9ed9a369", + "metadata": {}, + "outputs": [], + "source": [ + "import misinformation" + ] + }, + { + "cell_type": "markdown", + "id": "949d9f00-b129-477a-bc1d-e68fed73af2d", + "metadata": {}, + "source": [ + "We select a subset of image files to try facial expression detection on. The `find_files` function finds image files within a given directory:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "afe7e638-f09d-47e7-9295-1c374bd64c53", + "metadata": {}, + "outputs": [], + "source": [ + "images = misinformation.find_files()" + ] + }, + { + "cell_type": "markdown", + "id": "e149bfe5-90b0-49b2-af3d-688e41aab019", + "metadata": {}, + "source": [ + "If you want to fine tune the discovery of image files, you can provide more parameters:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f38bb8ed-1004-4e33-8ed6-793cb5869400", + "metadata": {}, + "outputs": [], + "source": [ + "?misinformation.find_files" + ] + }, + { + "cell_type": "markdown", + "id": "d8067ad1-ef8a-4e91-bcc6-f8dbef771854", + "metadata": {}, + "source": [ + "Next, we display the face recognition results provided by the DeepFace library. Click on the tabs to see the results in the right sidebar:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "992499ed-33f1-4425-ad5d-738cf565d175", + "metadata": {}, + "outputs": [], + "source": [ + "misinformation.explore_face_recognition(images)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.5" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..6fb8b8b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,29 @@ +[build-system] +requires = [ + "setuptools>=61", +] +build-backend = "setuptools.build_meta" + +[project] +name = "misinformation" +version = "0.0.1" +description = "Misinformation campaign analysis" +readme = "README.md" +maintainers = [ + { name = "Inga Ulusoy", email = "ssc@iwr.uni-heidelberg.de" }, + { name = "Dominic Kempf", email = "ssc@iwr.uni-heidelberg.de" }, +] +requires-python = ">=3.8" +license = { text = "MIT" } +classifiers = [ + "Programming Language :: Python :: 3", + "Operating System :: OS Independent", + "License :: OSI Approved :: MIT License", +] +dependencies = [ + "deepface", + "ipywidgets ==8.0.0rc1", +] + +[tool.setuptools] +packages = ["misinformation"] \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..b024da8 --- /dev/null +++ b/setup.py @@ -0,0 +1,4 @@ +from setuptools import setup + + +setup()