Repository: boltgolt/howdy Branch: master Commit: d3ab99382f88 Files: 98 Total size: 276.1 KB Directory structure: gitextract_mkbt1aje/ ├── .clang-tidy ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ ├── pull_request_template.md │ └── workflows/ │ └── check.yml ├── .gitignore ├── LICENSE ├── README.md ├── howdy/ │ ├── archlinux/ │ │ ├── .gitignore │ │ └── howdy/ │ │ ├── .gitignore │ │ └── PKGBUILD │ ├── debian/ │ │ ├── changelog │ │ ├── compat │ │ ├── control │ │ ├── copyright │ │ ├── howdy.lintian-overrides │ │ ├── howdy.manpages │ │ ├── install │ │ ├── postinst │ │ ├── preinst │ │ ├── prerm │ │ ├── rules │ │ └── source/ │ │ ├── format │ │ └── options │ ├── howdy.1 │ ├── meson.build │ └── src/ │ ├── autocomplete/ │ │ └── howdy.in │ ├── bin/ │ │ └── howdy.in │ ├── cli/ │ │ ├── __init__.py │ │ ├── add.py │ │ ├── clear.py │ │ ├── config.py │ │ ├── disable.py │ │ ├── list.py │ │ ├── remove.py │ │ ├── set.py │ │ ├── snap.py │ │ └── test.py │ ├── cli.py │ ├── compare.py │ ├── config.ini │ ├── dlib-data/ │ │ ├── .gitignore │ │ ├── Readme.md │ │ └── install.sh │ ├── i18n.py │ ├── meson.build │ ├── pam/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── enter_device.cc │ │ ├── enter_device.hh │ │ ├── main.cc │ │ ├── main.hh │ │ ├── meson.build │ │ ├── optional_task.hh │ │ ├── paths.hh.in │ │ └── po/ │ │ ├── LINGUAS │ │ ├── POTFILES │ │ └── meson.build │ ├── pam-config/ │ │ └── howdy.in │ ├── paths.py.in │ ├── paths_factory.py │ ├── recorders/ │ │ ├── __init__.py │ │ ├── ffmpeg_reader.py │ │ ├── pyv4l2_reader.py │ │ ├── v4l2.py │ │ └── video_capture.py │ ├── rubberstamps/ │ │ ├── __init__.py │ │ ├── hotkey.py │ │ └── nod.py │ └── snapshot.py ├── howdy-gtk/ │ ├── bin/ │ │ └── howdy-gtk.in │ ├── debian/ │ │ ├── changelog │ │ ├── compat │ │ ├── control │ │ ├── copyright │ │ ├── howdy-gtk.links │ │ ├── howdy-gtk.lintian-overrides │ │ ├── install │ │ ├── postinst │ │ ├── rules │ │ └── source/ │ │ ├── format │ │ └── options │ ├── meson.build │ └── src/ │ ├── authsticky.py │ ├── i18n.py │ ├── init.py │ ├── main.glade │ ├── onboarding.glade │ ├── onboarding.py │ ├── paths.py.in │ ├── paths_factory.py │ ├── polkit/ │ │ └── com.github.boltgolt.howdy-gtk.policy.in │ ├── tab_models.py │ ├── tab_video.py │ └── window.py ├── meson.build └── meson.options ================================================ FILE CONTENTS ================================================ ================================================ FILE: .clang-tidy ================================================ Checks: 'clang-diagnostic-*,clang-analyser-*,-clang-diagnostic-unused-command-line-argument,google-*,bugprone-*,modernize-*,performance-*,portability-*,readability-*,-bugprone-easily-swappable-*,-readability-magic-numbers,-google-readability-todo' WarningsAsErrors: 'clang-diagnostic-*,clang-analyser-*,-clang-diagnostic-unused-command-line-argument,google-*,bugprone-*,modernize-*,performance-*,portability-*,readability-*,-bugprone-easily-swappable-*,-readability-magic-numbers,-google-readability-todo' CheckOptions: - key: readability-function-cognitive-complexity.Threshold value: '50' # vim:syntax=yaml ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms custom: https://www.buymeacoffee.com/boltgolt ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Report something that's not working --- _Please describe the issue in as much detail as possible, including any errors and traces._ _If your issue is a camera issue, be sure to also post the image generated by running `sudo howdy snapshot`._ ---- I've searched for similar issues already, and my issue has not been reported yet. Linux distribution (if applicable): Howdy version (`sudo howdy version`): ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: 'Suggest a feature or improvement ' --- ================================================ FILE: .github/pull_request_template.md ================================================ _Please make sure to target the "dev" branch if it exists_ _REMOVE THIS MESSAGE IN THE PULL REQUEST_ ================================================ FILE: .github/workflows/check.yml ================================================ name: check on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - name: Install required libraries run: > sudo apt-get update && sudo apt-get install -y python3 python3-pip python3-setuptools python3-wheel cmake make build-essential clang-tidy libpam0g-dev libinih-dev libevdev-dev python3-dev libopencv-dev - name: Install meson run: sudo python3 -m pip install meson ninja - uses: actions/checkout@v2 - name: Build run: | meson setup build ninja -C build - name: Check source code run: | ninja clang-tidy -C build ================================================ FILE: .gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg # 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/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # dotenv .env # virtualenv .venv venv/ ENV/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ # generated models /howdy/src/models # snapshots /howdy/src/snapshots # build files debian/howdy.substvars debian/files debian/debhelper-build-stamp debian/howdy # vscode .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json !.vscode/*.code-snippets # Local History for Visual Studio Code .history/ # Built Visual Studio Code Extensions *.vsix # Meson subprojects/ ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2018 boltgolt Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ ![](https://boltgolt.nl/howdy/banner.png)

Howdy provides Windows Hello™ style authentication for Linux. Use your built-in IR emitters and camera in combination with facial recognition to prove who you are. Using the central authentication system (PAM), this works everywhere you would otherwise need your password: Login, lock screen, sudo, su, etc. ## Installation Howdy is currently available and packaged for Debian/Ubuntu, Arch Linux, Fedora and openSUSE. If you’re interested in packaging Howdy for your distro, don’t hesitate to open an issue. **Note:** The build of dlib can hang on 100% for over a minute, give it time. ### Ubuntu or Linux Mint Run the installer by pasting (`ctrl+shift+V`) the following commands into the terminal one at a time: ``` sudo add-apt-repository ppa:boltgolt/howdy sudo apt update sudo apt install howdy ``` This will guide you through the installation. ### Debian Download the .deb file from the [Releases page](https://github.com/boltgolt/howdy/releases) and install with gdebi. ### Arch Linux _Maintainer wanted._ Install the `howdy` package from the AUR. For AUR installation instructions, take a look at this [wiki page](https://wiki.archlinux.org/index.php/Arch_User_Repository#Installing_packages). You will need to do some additional configuration steps. Please read the [ArchWiki entry](https://wiki.archlinux.org/index.php/Howdy) for more information. ### Fedora _Maintainer: [@luyatshimbalanga](https://github.com/luyatshimbalanga)_ The `howdy` package is available as a [Fedora COPR repository](https://copr.fedorainfracloud.org/coprs/principis/howdy/), install it by simply executing the following commands in a terminal: ``` sudo dnf copr enable principis/howdy sudo dnf --refresh install howdy ``` *Note:* Fedora 41 [removed support for Python2](https://fedoraproject.org/wiki/Changes/RetirePython2.7), but at this point in time Howdy still depends on it. If the install fails, you can fix this by installing the beta Repository and removing the release version: ``` sudo dnf copr remove principis/howdy sudo dnf copr enable principis/howdy-beta sudo dnf --refresh install howdy ``` See the link to the COPR repository for detailed configuration steps. ### openSUSE _Maintainer: [@dmafanasyev](https://github.com/dmafanasyev)_ Go to the [openSUSE wiki page](https://en.opensuse.org/SDB:Facial_authentication) for detailed installation instructions. ### Building from source If you want to build Howdy from source, a few dependencies are required. #### Dependencies - Python 3.6 or higher * pip * setuptools * wheel - meson version 0.64 or higher - ninja - INIReader (can be pulled from git automatically if not found) - libevdev To install them on Debian/Ubuntu for example: ``` sudo apt-get update && sudo apt-get install -y \ python3 python3-pip python3-setuptools python3-wheel \ cmake make build-essential \ libpam0g-dev libinih-dev libevdev-dev python3-opencv \ python3-dev libopencv-dev ``` #### Build ```sh meson setup build meson compile -C build ``` You can also install Howdy to your system with `meson install -C build`. ## Setup After installation, Howdy needs to learn what you look like so it can recognise you later. Run `sudo howdy add` to add a face model. If nothing went wrong we should be able to run sudo by just showing your face. Open a new terminal and run `sudo -i` to see it in action. Please check [this wiki page](https://github.com/boltgolt/howdy/wiki/Common-issues) if you're experiencing problems or [search](https://github.com/boltgolt/howdy/issues) for similar issues. If you're curious you can run `sudo howdy config` to open the central config file and see the options Howdy has to offer. On most systems this will open the nano editor, where you have to press `ctrl`+`x` to save your changes. ## CLI The installer adds a `howdy` command to manage face models for the current user. Use `howdy --help` or `man howdy` to list the available options. Usage: ``` howdy [-U user] [-y] command [argument] ``` | Command | Description | |-----------|-----------------------------------------------| | `add` | Add a new face model for a user | | `clear` | Remove all face models for a user | | `config` | Open the config file in your default editor | | `disable` | Disable or enable howdy | | `list` | List all saved face models for a user | | `remove` | Remove a specific model for a user | | `snapshot`| Take a snapshot of your camera input | | `test` | Test the camera and recognition methods | | `version` | Print the current version number | ## Contributing [![](https://img.shields.io/travis/boltgolt/howdy/dev.svg?label=dev%20build)](https://github.com/boltgolt/howdy/tree/dev) [![](https://img.shields.io/github/issues-raw/boltgolt/howdy/enhancement.svg?label=feature+requests&colorB=4c1)](https://github.com/boltgolt/howdy/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement) The easiest ways to contribute to Howdy is by starring the repository and opening GitHub issues for features you'd like to see. If you want to do more, you can also [buy me a coffee](https://www.buymeacoffee.com/boltgolt). Code contributions are also very welcome. If you want to port Howdy to another distro, feel free to open an issue for that too. ## Troubleshooting Any Python errors get logged directly into the console and should indicate what went wrong. If authentication still fails but no errors are printed, you could take a look at the last lines in `/var/log/auth.log` to see if anything has been reported there. Please first check the [wiki on common issues](https://github.com/boltgolt/howdy/wiki/Common-issues) and if you encounter an error that hasn't been reported yet, don't be afraid to open a new issue. ## A note on security This package is in no way as secure as a password and will never be. Although it's harder to fool than normal face recognition, a person who looks similar to you, or a well-printed photo of you could be enough to do it. Howdy is a more quick and convenient way of logging in, not a more secure one. To minimize the chance of this program being compromised, it's recommended to leave Howdy in `/lib/security` and to keep it read-only. DO NOT USE HOWDY AS THE SOLE AUTHENTICATION METHOD FOR YOUR SYSTEM. ================================================ FILE: howdy/archlinux/.gitignore ================================================ pkg src *.tar.gz *.zip *.tar.xz *.patch *.dat.bz2 ================================================ FILE: howdy/archlinux/howdy/.gitignore ================================================ pkg src *.tar.gz *.zip *.tar.xz *.patch *.dat.bz2 .SRCINFO ================================================ FILE: howdy/archlinux/howdy/PKGBUILD ================================================ # Maintainer: Frank Tackitt # Maintainer: boltgolt # Co-Maintainer: Raymo111 # Contributor: Kelley McChesney pkgname=howdy pkgver=2.6.1 pkgrel=1 pkgdesc="Windows Hello for Linux" arch=('x86_64') url="https://github.com/boltgolt/howdy" license=('MIT') depends=( 'opencv' 'hdf5' 'pam-python' 'python3' 'python-dlib' 'python-numpy' 'python-opencv' ) makedepends=( 'cmake' 'pkgfile' ) backup=('etc/howdy/config.ini') source=( "$pkgname-$pkgver.tar.gz::https://github.com/boltgolt/howdy/archive/v${pkgver}.tar.gz" "https://github.com/davisking/dlib-models/raw/master/dlib_face_recognition_resnet_model_v1.dat.bz2" "https://github.com/davisking/dlib-models/raw/master/mmod_human_face_detector.dat.bz2" "https://github.com/davisking/dlib-models/raw/master/shape_predictor_5_face_landmarks.dat.bz2" ) sha256sums=('f3f48599f78fd82b049539fcfc34de25c9435cad732697bdda94e85352964794' 'abb1f61041e434465855ce81c2bd546e830d28bcbed8d27ffbe5bb408b11553a' 'db9e9e40f092c118d5eb3e643935b216838170793559515541c56a2b50d9fc84' '6e787bbebf5c9efdb793f6cd1f023230c4413306605f24f299f12869f95aa472') package() { # Installing the proper license files and the rest of howdy cd howdy-$pkgver install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE" mkdir -p "$pkgdir/usr/etc/howdy" mkdir -p "$pkgdir/etc/howdy" cp -r src/* "$pkgdir/usr/etc/howdy" cp -r src/config.ini "$pkgdir/etc/howdy" cp "${srcdir}/dlib_face_recognition_resnet_model_v1.dat" "$pkgdir/usr/etc/howdy/dlib-data/" cp "${srcdir}/mmod_human_face_detector.dat" "$pkgdir/usr/etc/howdy/dlib-data/" cp "${srcdir}/shape_predictor_5_face_landmarks.dat" "$pkgdir/usr/etc/howdy/dlib-data/" chmod 600 -R "$pkgdir/usr/etc/howdy" mkdir -p "$pkgdir/usr/bin" ln -s /etc/howdy/cli.py "$pkgdir/usr/bin/howdy" chmod +x "$pkgdir/etc/howdy/cli.py" mkdir -p "$pkgdir/usr/share/bash-completion/completions" cp autocomplete/howdy "$pkgdir/usr/share/bash-completion/completions/howdy" } ================================================ FILE: howdy/debian/changelog ================================================ howdy (3.0.0) focal; urgency=medium * Way too many changes to all list individually, thanks to everyone who contributed! * Rewrote PAM handling logic in C++ (thanks @saidsay-so!) * Added simultaneous face recognition and password authentication * Added native dialog dismissal after successful authentication * Added configurable image rotation (thanks @matan-arnon!) * Fixed four config options whose names were opposite of their function -- boltgolt Sun, 22 Jun 2025 11:52:44 +0200 howdy (2.6.1) xenial; urgency=medium * Fixed accidentally using emergency priority for log messages (thanks @kageurufu and many others!) * Fixed certainty prompt selected the exact opposite value * Fixed sleeping for negative time in test slow mode (thanks @willwill2will54!) * Fixed opencv error when imported after dlib (thanks @cnyk!) * Fixed typo causing manual exposure failure (thanks @h45h74x!) * Fixed missing command autocomplete options on tab * Fixed not knowing how to spell the word latest (thanks @divykj!) -- boltgolt Wed, 02 Sep 2020 15:05:59 +0200 howdy (2.6.0) xenial; urgency=medium * Added new options to capture a snapshot of failed or even successful logins * Added command that creates a new snapshot and saves it * Added version command * Added question to automatically set certainty value on installation * Added automatic logging to system-wide auth.log * Added clearer feedback when login is rejected due to dark frames (thanks @andrewmv!) * Refactored video capture logic (thanks @AnthonyWharton!) * Reordered the editor priorities for the config command * Fixed gstreamer warnings showing up in console (thanks @ajnart!) * Fixed issue where add command would never end * Fixed test command overlay not being in color (thanks @PetePriority!) * Fixed typo preventing timeout config option from working (thanks @Ajayneethikannan!) * Fixed old numpy installation failure (thanks @rushabh-v!) * Fixed issue where no PAM response would be returned * Fixed CLAHE not being applied equally to all video commands (thanks @PetePriority!) * Fixed an incorrect suggested command (thanks @TheButlah!) * Fixed missing release method in video capture class * Removed deprecated dlib flags (thanks @rhysperry111!) * Removed streamer as a required dependency -- boltgolt Mon, 22 Jun 2020 16:11:46 +0200 howdy (2.5.1) xenial; urgency=medium * Removed dismiss_lockscreen as it could lock users out of their system (thanks @ujjwalbe, @ju916 and many others!) * Added option to disable howdy when the laptop lid is closed (thanks @accek!) * Added automatic fallback to default frame color palette (thanks @Ethiarpus!) * Added manual exposure setting (thanks @accek!) * Fixed test command ignoring dark frame threshold (thanks @eduncan911!) * Fixed import error in v4l2 recorder (thanks @timwelch!) -- boltgolt Fri, 29 Mar 2019 23:02:21 +0100 howdy (2.5.0) xenial; urgency=medium * Added FFmpeg and v4l2 recorders (thanks @timwelch!) * Added automatic PAM inclusion on installation * Added optional notice on detection attempt (thanks @mrkmg!) * Added support for grayscale frame encoding (thanks @dmig and @sapjunior!) * Massively improved recognition speed (thanks @dmig!) * Fixed typo in "timout" config value * Removed unneeded dependencies (thanks @dmig!) -- boltgolt Sun, 06 Jan 2019 14:37:41 +0100 howdy (2.4.0) xenial; urgency=medium * Cameras are now selected by path instead of by video device number (thanks @Rhiyo!) * Added fallbacks to $EDITOR for the config command (thanks @yassineim!) * Fixed missing cv2 module after installation (thanks @bendandersen and many others!) * Fixed file permissions crashing Howdy in some cases (thanks @GJDitchfield!) * Fixed howdy using python3 from local virtual environment (thanks @EdwardJB!) -- boltgolt Fri, 09 Nov 2018 20:59:45 +0100 howdy (2.3.1) xenial; urgency=high * Fixed issue where `frame_width` and `frame_height` would be completely ignored (thanks @janecz-n!) * Fixed security problem with remote session authentication (thanks @cccaballero!) -- boltgolt Mon, 24 Sep 2018 17:49:07 +0100 howdy (2.3.0) xenial; urgency=medium * Added a config option to set the frame height and width (thanks @wzrdtales!) * Rewrote the code that fetches the non-root username (thanks @dmig!) * Changed the config command so it uses the default editor (thanks @stellarpower and @dmig!) * Fixed issue where a "y" could be interpreted as a no (thanks @ramkrishna757575!) * Fixed division by zeno (thanks @stellarpower!) -- boltgolt Thu, 28 Jun 2018 14:59:52 +0100 howdy (2.2.2) xenial; urgency=medium * Fixed fetching of wrong config section (thanks @halcyoncheng and @arifeinberg!) -- boltgolt Fri, 11 May 2018 10:43:03 +0200 howdy (2.2.1) xenial; urgency=medium * Added mechanism to keep config files between updates * Added force_mjpeg option to fix YUYV image issues (thanks @arifeinberg!) * Revamped the bash autocompletion script * Fixed timeout never being reached in certain scenarios (thanks @Tkopic001!) * Fixed issue where BGR to RGB frame conversion caused a crash (thanks @Jerezano!) -- boltgolt Thu, 10 May 2018 15:14:03 +0200 howdy (2.1.0) xenial; urgency=medium * First complete PPA release * Reworked CLI -- boltgolt Fri, 13 Apr 2018 22:22:27 +0200 howdy (2.0.0-alpha+3) xenial; urgency=medium * Fixed issue where dlib dependency failed to install on some installations * Added preinst script for camera detection -- boltgolt Thu, 12 Apr 2018 21:42:42 +0000 howdy (2.0.0-alpha+2) xenial; urgency=medium * Fixed build dependency issue -- boltgolt Sat, 07 Apr 2018 21:30:48 +0200 howdy (2.0.0-alpha+1) xenial; urgency=low * Initial packaged release. -- boltgolt Wed, 04 Apr 2018 18:13:15 +0200 ================================================ FILE: howdy/debian/compat ================================================ 10 ================================================ FILE: howdy/debian/control ================================================ Source: howdy Section: misc Priority: optional Standards-Version: 3.9.7 Build-Depends: devscripts, git, dh-make, debhelper, fakeroot, python3, python3-pip, python3-setuptools, python3-wheel, ninja-build, meson, libpam0g-dev, libboost-all-dev, pkg-config, libevdev-dev, libinih-dev Maintainer: boltgolt Vcs-Git: https://github.com/boltgolt/howdy Package: howdy Homepage: https://github.com/boltgolt/howdy Architecture: amd64 Depends: ${misc:Depends}, libc6, libgcc-s1, libpam0g, libstdc++6, curl | wget, python3, python3-pip, python3-dev, python3-setuptools, python3-numpy, python-opencv | python3-opencv, libopencv-dev, cmake, libinih-dev Recommends: libatlas-base-dev | libopenblas-dev | liblapack-dev, howdy-gtk, v4l-utils Suggests: nvidia-cuda-dev (>= 7.5) Description: Howdy: Windows Hello style authentication for Linux. Use your built-in IR emitters and camera in combination with face recognition to prove who you are. ================================================ FILE: howdy/debian/copyright ================================================ MIT License Copyright (c) 2018 boltgolt Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: howdy/debian/howdy.lintian-overrides ================================================ # W: Don't require ugly linebreaks in last 5 chars howdy: debian-changelog-line-too-long # E: Allows the name Howdy to show up in Ubuntu updater howdy: description-starts-with-package-name # E: Allows python for installation scripts howdy: unknown-control-interpreter ================================================ FILE: howdy/debian/howdy.manpages ================================================ howdy.1 ================================================ FILE: howdy/debian/install ================================================ src/cli/. lib/security/howdy/cli src/locales/. lib/security/howdy/locales src/recorders/. lib/security/howdy/recorders src/rubberstamps/. lib/security/howdy/rubberstamps src/cli.py lib/security/howdy src/compare.py lib/security/howdy src/i18n.py lib/security/howdy src/logo.png lib/security/howdy src/snapshot.py lib/security/howdy build/pam_howdy.so lib/security/howdy src/dlib-data/. etc/howdy/dlib-data src/config.ini etc/howdy src/autocomplete/. usr/share/bash-completion/completions src/pam-config/. /usr/share/pam-configs ================================================ FILE: howdy/debian/postinst ================================================ #!/usr/bin/python3 # Installation script to install howdy # Executed after primary apt install # Import required modules import fileinput import subprocess import sys import os import re import tarfile from shutil import rmtree, which # Don't run unless we need to configure the install # Will also happen on upgrade but we will catch that later on if "configure" not in sys.argv: sys.exit(0) def log(text): """Print a nicely formatted line to stdout""" print("\n>>> " + col(1) + text + col(0) + "\n") def handleStatus(status): """Abort if a command fails""" if (status != 0): print(col(3) + "Error while running last command" + col(0)) sys.exit(1) def col(id): """Add color escape sequences""" if id == 1: return "\033[32m" if id == 2: return "\033[33m" if id == 3: return "\033[31m" return "\033[0m" # Create shorthand for subprocess creation sc = subprocess.call # If the package is being upgraded if "upgrade" in sys.argv: # If preinst has made a config backup if os.path.exists("/tmp/howdy_config_backup_v" + sys.argv[2] + ".ini"): # Get the config parser import configparser # Load th old and new config files oldConf = configparser.ConfigParser() oldConf.read("/tmp/howdy_config_backup_v" + sys.argv[2] + ".ini") newConf = configparser.ConfigParser() newConf.read("/etc/howdy/config.ini") # Go through every setting in the old config and apply it to the new file for section in oldConf.sections(): for (key, value) in oldConf.items(section): # MIGRATION 2.3.1 -> 2.4.0 # If config is still using the old device_id parameter, convert it to a path if key == "device_id": key = "device_path" value = "/dev/video" + value # MIGRATION 2.4.0 -> 2.5.0 # Finally correct typo in "timout" config value if key == "timout": key = "timeout" # MIGRATION 2.5.0 -> 2.5.1 # Remove unsafe automatic dismissal of lock screen if key == "dismiss_lockscreen": if value == "true": print("DEPRECATION: Config value dismiss_lockscreen is no longer supported because of login loop issues.") continue # MIGRATION 2.6.1 -> 3.0.0 # Fix capture being enabled by default if key == "capture_failed" or key == "capture_successful": if value == "true": print("NOTICE: Howdy login image captures have been disabled by default, change the config to enable them again") value = "false" # MIGRATION 2.6.1 -> 3.0.0 # Rename config options so they don't do the opposite of what is commonly expected if key == "ignore_ssh": key = "abort_if_ssh" if key == "ignore_closed_lid": key = "abort_if_lid_closed" if key == "capture_failed": key = "save_failed" if key == "capture_successful": key = "save_successful" try: newConf.set(section, key, value) # Add a new section where needed except configparser.NoSectionError: newConf.add_section(section) newConf.set(section, key, value) # Write it all to file with open("/etc/howdy/config.ini", "w") as configfile: newConf.write(configfile) sys.exit(0) log("Downloading dlib") dlib_archive = "/tmp/v19.16.tar.gz" loader = which("wget") LOADER_CMD = None # If wget is installed, use that as the downloader if loader: LOADER_CMD = [loader, "--tries", "5", "--output-document"] # Otherwise, fall back on curl else: loader = which("curl") LOADER_CMD = [loader, "--retry", "5", "--location", "--output"] # Assemble and execute the download command cmd = LOADER_CMD + [dlib_archive, "https://github.com/davisking/dlib/archive/v19.16.tar.gz"] handleStatus(sc(cmd)) # The folder containing the dlib source DLIB_DIR = None # A regex of all files to ignore while unpacking the archive excludes = re.compile( r"davisking-dlib-\w+/(dlib/(http_client|java|matlab|test/)|" r"(docs|examples|python_examples)|" r"tools/(archive|convert_dlib_nets_to_caffe|htmlify|imglab|python/test|visual_studio_natvis))" ) # Open the archive with tarfile.open(dlib_archive) as tf: for item in tf: # Set the destination dir if unset if not DLIB_DIR: DLIB_DIR = "/tmp/" + item.name # extract only files sufficient for building if not excludes.match(item.name): tf.extract(item, "/tmp") # Delete the downloaded archive os.unlink(dlib_archive) log("Building dlib") cmd = ["sudo", "python3", "setup.py", "install"] cuda_used = False # Compile and link dlib try: sp = subprocess.Popen(cmd, cwd=DLIB_DIR, stdout=subprocess.PIPE) except subprocess.CalledProcessError: print("Error while building dlib") raise # Go through each line from stdout to check for CUDA usage while sp.poll() is None: line = sp.stdout.readline().decode("utf-8") if "DLIB WILL USE CUDA" in line: cuda_used = True print(line, end="") log("Cleaning up dlib") # Remove the no longer needed git clone del sp rmtree(DLIB_DIR) print("Temporary dlib files removed") log("Configuring howdy") # Make sure to use CNN if dlib was compiled with CUDA support for line in fileinput.input(["/etc/howdy/config.ini"], inplace=1): line = line.replace("use_cnn = false", "use_cnn = " + str(cuda_used).lower()) print(line, end="") print("CNN saved to config, CUDA " + ("enabled" if cuda_used else "disabled")) # Secure the howdy folder handleStatus(sc(["chmod 755 -R /lib/security/howdy/"], shell=True)) handleStatus(sc(["chmod 755 -R /etc/howdy/"], shell=True)) # Allow anyone to execute the python CLI os.chmod("/lib/security/howdy", 0o755) os.chmod("/lib/security/howdy/cli.py", 0o755) handleStatus(sc(["chmod 755 -R /lib/security/howdy/cli"], shell=True)) print("Permissions set") # Make the CLI executable as howdy os.symlink("/lib/security/howdy/cli.py", "/usr/local/bin/howdy") os.chmod("/usr/local/bin/howdy", 0o755) print("Howdy command installed") log("Adding howdy as PAM module") # Activate the pam-config file handleStatus(subprocess.call(["pam-auth-update --package"], shell=True)) # Sign off print("Installation complete.") ================================================ FILE: howdy/debian/preinst ================================================ #!/usr/bin/python3 # Used to check cameras before committing to install # Executed before primary apt install of files import subprocess import sys import os # Backup the config file if we're upgrading if "upgrade" in sys.argv: # Try to copy the config file as a backup try: # Try to copy the new location first if os.path.exists("/etc/howdy/config.ini"): subprocess.call(["cp /etc/howdy/config.ini /tmp/howdy_config_backup_v" + sys.argv[2] + ".ini"], shell=True) # If that does not exist, try copying the old location else: subprocess.call(["cp /lib/security/howdy/config.ini /tmp/howdy_config_backup_v" + sys.argv[2] + ".ini"], shell=True) # Let the user know so he knows where to look on a failed install print("Backup of Howdy config file created in /tmp/howdy_config_backup_v" + sys.argv[2] + ".ini") except subprocess.CalledProcessError: print("Could not make an backup of old Howdy config file") # Don't continue setup when we're just upgrading sys.exit(0) # Don't run if we're not trying to install fresh if "install" not in sys.argv: sys.exit(0) ================================================ FILE: howdy/debian/prerm ================================================ #!/usr/bin/python3 # Executed on deinstallation # Completely remove howdy from the system # Import required modules import subprocess import sys import os from shutil import rmtree # Only run when we actually want to remove if "remove" not in sys.argv and "purge" not in sys.argv: sys.exit(0) # Don't try running this if it's already gone if not os.path.exists("/lib/security/howdy/cli"): sys.exit(0) # Remove files and symlinks try: os.unlink("/usr/local/bin/howdy") except Exception: print("Can't remove executable") try: os.unlink("/usr/share/bash-completion/completions/howdy") except Exception: print("Can't remove autocompletion script") # Refresh and remove howdy from pam-config try: subprocess.call(["pam-auth-update --package"], shell=True) subprocess.call(["rm /usr/share/pam-configs/howdy"], shell=True) subprocess.call(["pam-auth-update --package"], shell=True) except Exception: print("Can't remove pam module") # Remove full installation folder, just to be sure try: rmtree("/lib/security/howdy") except Exception: # This error is normal pass # Remove dlib subprocess.call(["pip3", "uninstall", "dlib", "-y", "--no-cache-dir"]) ================================================ FILE: howdy/debian/rules ================================================ #!/usr/bin/make -f DH_VERBOSE = 1 DPKG_EXPORT_BUILDFLAGS = 1 include /usr/share/dpkg/default.mk %: dh $@ build: # Create build dir meson setup -Dinih:with_INIReader=true build src/pam # Compile shared object ninja -C build clean: # Delete mason build directory rm -rf ./build # Force remove temp debian build directory rm -rf ./debian/howdy # Make sure subprojects get pulled locally meson subprojects download --sourcedir src/pam ================================================ FILE: howdy/debian/source/format ================================================ 3.0 (native) ================================================ FILE: howdy/debian/source/options ================================================ tar-ignore = ".git" tar-ignore = ".gitignore" tar-ignore = ".github" tar-ignore = "models" tar-ignore = "snapshots" tar-ignore = "tests" tar-ignore = "README.md" tar-ignore = ".travis.yml" tar-ignore = "fedora" tar-ignore = "opensuse" tar-ignore = "archlinux" tar-ignore = "build" tar-ignore = "__pycache__" tar-ignore = "*.dat" ================================================ FILE: howdy/howdy.1 ================================================ .\" Please adjust this date whenever revising the manpage. .TH HOWDY 1 "April 9, 2018" "Howdy help" "User Commands" .SH NAME howdy \- Windows Hello style authentication for Linux .SH DESCRIPTION Howdy IR face recognition implements a PAM module to use your face as a authentication method. .SS "Usage:" .IP howdy [\-U USER] [\-y] [\-h] command [argument] .SS "Commands:" .TP add Add a new face model for an user. .TP clear Remove all face models for an user. .TP config Open the config file in gedit. .TP disable Disable or enable howdy. .TP list List all saved face models for an user. .TP remove Remove a specific model for an user. .TP clear Remove all face models for an user. .TP test Test the camera and recognition methods. .SS "Optional arguments:" .TP \fB\-U\fR USER, \fB\-\-user\fR USER Set the user account to use. .TP \fB\-y\fR Skip all questions. .TP \fB\-h\fR, \fB\-\-help\fR Show this help message and exit. .PP .SH AUTHOR Howdy was written by boltgolt. For more information visit https://github.com/boltgolt/howdy ================================================ FILE: howdy/meson.build ================================================ subdir('src') ================================================ FILE: howdy/src/autocomplete/howdy.in ================================================ #!/bin/bash # Autocomplete file run in bash # Will sugest arguments on tab _howdy() { local cur prev opts local config_path="@config_path@" COMPREPLY=() # The argument typed so far cur="${COMP_WORDS[COMP_CWORD]}" # The previous argument prev="${COMP_WORDS[COMP_CWORD-1]}" # Go though all cases we support case "${prev}" in # After the main command, show the commands "howdy") opts="add clear config disable list remove clear snapshot test version" COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) return 0 ;; # For disable, grab the current "disabled" config option and give the reverse "disable") local status=$(cut -d'=' -f2 <<< $(cat $config_path | grep 'disabled =') | xargs echo -n) [ "$status" == "false" ] && COMPREPLY="true" || COMPREPLY="false" return 0 ;; # List the users available "-U") COMPREPLY=( $(compgen -u -- ${cur}) ) return 0 ;; "--user") COMPREPLY=( $(compgen -u -- ${cur}) ) return 0 ;; *) ;; esac # Nothing matched, so return nothing return 0 } # Register the autocomplete function complete -F _howdy howdy ================================================ FILE: howdy/src/bin/howdy.in ================================================ #!/bin/sh @python_path@ "@script_path@" "$@" ================================================ FILE: howdy/src/cli/__init__.py ================================================ # Marks this folder as importable ================================================ FILE: howdy/src/cli/add.py ================================================ # Save the face of the user in encoded form # Import required modules import time import os import sys import json import configparser import builtins import numpy as np import paths_factory from recorders.video_capture import VideoCapture from i18n import _ # Try to import dlib and give a nice error if we can't # Add should be the first point where import issues show up try: import dlib except ImportError as err: print(err) print(_("\nCan't import the dlib module, check the output of")) print("pip3 show dlib") sys.exit(1) # OpenCV needs to be imported after dlib import cv2 # Test if at lest 1 of the data files is there and abort if it's not if not os.path.isfile(paths_factory.shape_predictor_5_face_landmarks_path()): print(_("Data files have not been downloaded, please run the following commands:")) print("\n\tcd " + paths_factory.dlib_data_dir_path()) print("\tsudo ./install.sh\n") sys.exit(1) # Read config from disk config = configparser.ConfigParser() config.read(paths_factory.config_file_path()) use_cnn = config.getboolean("core", "use_cnn", fallback=False) if use_cnn: face_detector = dlib.cnn_face_detection_model_v1(paths_factory.mmod_human_face_detector_path()) else: face_detector = dlib.get_frontal_face_detector() pose_predictor = dlib.shape_predictor(paths_factory.shape_predictor_5_face_landmarks_path()) face_encoder = dlib.face_recognition_model_v1(paths_factory.dlib_face_recognition_resnet_model_v1_path()) user = builtins.howdy_user # The permanent file to store the encoded model in enc_file = paths_factory.user_model_path(user) # Known encodings encodings = [] # Make the ./models folder if it doesn't already exist if not os.path.exists(paths_factory.user_models_dir_path()): print(_("No face model folder found, creating one")) os.makedirs(paths_factory.user_models_dir_path()) # To try read a premade encodings file if it exists try: encodings = json.load(open(enc_file)) except FileNotFoundError: encodings = [] # Print a warning if too many encodings are being added if len(encodings) > 3: print(_("NOTICE: Each additional model slows down the face recognition engine slightly")) print(_("Press Ctrl+C to cancel\n")) # Make clear what we are doing if not human if not builtins.howdy_args.plain: print(_("Adding face model for the user ") + user) # Set the default label label = "Initial model" # some id's can be skipped, but the last id is always the maximum next_id = encodings[-1]["id"] + 1 if encodings else 0 # Get the label from the cli arguments if provided if builtins.howdy_args.arguments: label = builtins.howdy_args.arguments[0] # Or set the default label else: label = _("Model #") + str(next_id) # Keep de default name if we can't ask questions if builtins.howdy_args.y: print(_('Using default label "%s" because of -y flag') % (label, )) else: # Ask the user for a custom label label_in = input(_("Enter a label for this new model [{}]: ").format(label)) # Set the custom label (if any) and limit it to 24 characters if label_in != "": label = label_in[:24] # Remove illegal characters if "," in label: print(_("NOTICE: Removing illegal character \",\" from model name")) label = label.replace(",", "") # Prepare the metadata for insertion insert_model = { "time": int(time.time()), "label": label, "id": next_id, "data": [] } # Set up video_capture video_capture = VideoCapture(config) print(_("\nPlease look straight into the camera")) # Give the user time to read time.sleep(2) # Will contain found face encodings enc = [] # Count the number of read frames frames = 0 # Count the number of illuminated read frames valid_frames = 0 # Count the number of illuminated frames that # were rejected for being too dark dark_tries = 0 # Track the running darkness total dark_running_total = 0 face_locations = None dark_threshold = config.getfloat("video", "dark_threshold", fallback=60) clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) # Loop through frames till we hit a timeout while frames < 60: frames += 1 # Grab a single frame of video frame, gsframe = video_capture.read_frame() gsframe = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) gsframe = clahe.apply(gsframe) # Create a histogram of the image with 8 values hist = cv2.calcHist([gsframe], [0], None, [8], [0, 256]) # All values combined for percentage calculation hist_total = np.sum(hist) # Calculate frame darkness darkness = (hist[0] / hist_total * 100) # If the image is fully black due to a bad camera read, # skip to the next frame if (hist_total == 0) or (darkness == 100): continue # Include this frame in calculating our average session brightness dark_running_total += darkness valid_frames += 1 # If the image exceeds darkness threshold due to subject distance, # skip to the next frame if (darkness > dark_threshold): dark_tries += 1 continue # Get all faces from that frame as encodings face_locations = face_detector(gsframe, 1) # If we've found at least one, we can continue if face_locations: break video_capture.release() # If we've found no faces, try to determine why if not face_locations: if valid_frames == 0: print(_("Camera saw only black frames - is IR emitter working?")) elif valid_frames == dark_tries: print(_("All frames were too dark, please check dark_threshold in config")) print(_("Average darkness: {avg}, Threshold: {threshold}").format(avg=str(dark_running_total / valid_frames), threshold=str(dark_threshold))) else: print(_("No face detected, aborting")) sys.exit(1) # If more than 1 faces are detected we can't know which one belongs to the user elif len(face_locations) > 1: print(_("Multiple faces detected, aborting")) sys.exit(1) face_location = face_locations[0] if use_cnn: face_location = face_location.rect # Get the encodings in the frame face_landmark = pose_predictor(frame, face_location) face_encoding = np.array(face_encoder.compute_face_descriptor(frame, face_landmark, 1)) insert_model["data"].append(face_encoding.tolist()) # Insert full object into the list encodings.append(insert_model) # Save the new encodings to disk with open(enc_file, "w") as datafile: json.dump(encodings, datafile) # Give let the user know how it went print(_("""\nScan complete Added a new model to """) + user) ================================================ FILE: howdy/src/cli/clear.py ================================================ # Clear all models by deleting the whole file # Import required modules import os import sys import builtins import paths_factory from i18n import _ # Get the passed user user = builtins.howdy_user # Check if the models folder is there if not os.path.exists(paths_factory.user_models_dir_path()): print(_("No models created yet, can't clear them if they don't exist")) sys.exit(1) # Check if the user has a models file to delete if not os.path.isfile(paths_factory.user_model_path(user)): print(_("{} has no models or they have been cleared already").format(user)) sys.exit(1) # Only ask the user if there's no -y flag if not builtins.howdy_args.y: # Double check with the user print(_("This will clear all models for ") + user) ans = input(_("Do you want to continue [y/N]: ")) # Abort if they don't answer y or Y if (ans.lower() != "y"): print(_('\nInterpreting as a "NO", aborting')) sys.exit(1) # Delete otherwise os.remove(paths_factory.user_model_path(user)) print(_("\nModels cleared")) ================================================ FILE: howdy/src/cli/config.py ================================================ # Open the config file in an editor # Import required modules import os import subprocess import shutil import paths_factory from i18n import _ # Determine the editor to use editor = None preferred_editor = os.environ.get("EDITOR") nano_path = shutil.which("nano") vi_path = shutil.which("vi") # Use the user preferred editor if available if preferred_editor: if shutil.which(preferred_editor): editor = preferred_editor if not editor: if nano_path: editor = nano_path elif vi_path: editor = vi_path if editor: editor_name = os.path.basename(editor) # Let the user know what we're doing print(_("Opening config.ini in {editor}").format(editor=editor_name)) # Open the editor as a subprocess and fork it try: subprocess.call([editor, paths_factory.config_file_path()]) except Exception as e: print(_("Failed to open editor: {error}").format(error=e)) else: print(_("Error: Could not find a suitable text editor.")) print(_("Please install 'nano' or 'vi', or set the EDITOR environment variable.")) print(_("If you are running this command with sudo, try 'sudo -E howdy config' to preserve your EDITOR variable.")) ================================================ FILE: howdy/src/cli/disable.py ================================================ # Set the disable flag # Import required modules import sys import os import builtins import fileinput import configparser import paths_factory from i18n import _ # Get the absolute filepath config_path = paths_factory.config_file_path() # Read config from disk config = configparser.ConfigParser() config.read(config_path) # Check if enough arguments have been passed if not builtins.howdy_args.arguments: print(_("Please add a 0 (enable) or a 1 (disable) as an argument")) sys.exit(1) # Get the cli argument argument = builtins.howdy_args.arguments[0] # Translate the argument to the right string if argument == "1" or argument.lower() == "true": out_value = "true" elif argument == "0" or argument.lower() == "false": out_value = "false" else: # Of it's not a 0 or a 1, it's invalid print(_("Please only use 0 (enable) or 1 (disable) as an argument")) sys.exit(1) # Don't do anything when the state is already the requested one if out_value == config.get("core", "disabled", fallback=True): print(_("The disable option has already been set to ") + out_value) sys.exit(1) # Loop though the config file and only replace the line containing the disable config for line in fileinput.input([config_path], inplace=1): print(line.replace("disabled = " + config.get("core", "disabled", fallback=True), "disabled = " + out_value), end="") # Print what we just did if out_value == "true": print(_("Howdy has been disabled")) else: print(_("Howdy has been enabled")) ================================================ FILE: howdy/src/cli/list.py ================================================ # List all models for a user # Import required modules import sys import os import json import time import builtins import paths_factory from i18n import _ user = builtins.howdy_user # Check if the models file has been created yet if not os.path.exists(paths_factory.user_models_dir_path()): print(_("Face models have not been initialized yet, please run:")) print("\n\tsudo howdy -U " + user + " add\n") sys.exit(1) # Path to the models file enc_file = paths_factory.user_model_path(user) # Try to load the models file and abort if the user does not have it yet try: encodings = json.load(open(enc_file)) except FileNotFoundError: if not builtins.howdy_args.plain: print(_("No face model known for the user {}, please run:").format(user)) print("\n\tsudo howdy -U " + user + " add\n") sys.exit(1) # Print a header if we're not in plain mode if not builtins.howdy_args.plain: print(_("Known face models for {}:").format(user)) print("\n\033[1;29m" + _("ID Date Label\033[0m")) # Loop through all encodings and print info about them for enc in encodings: # Start with the id print(str(enc["id"]), end="") # Add comma for machine reading if builtins.howdy_args.plain: print(",", end="") # Print padding spaces after the id for a nice layout else: print((4 - len(str(enc["id"]))) * " ", end="") # Format the time as ISO in the local timezone print(time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(enc["time"])), end="") # Separate with commas again for machines, spaces otherwise print("," if builtins.howdy_args.plain else " ", end="") # End with the label print(enc["label"]) # Add a closing enter print() ================================================ FILE: howdy/src/cli/remove.py ================================================ # Remove a encoding from the models file # Import required modules import sys import os import json import builtins import paths_factory from i18n import _ user = builtins.howdy_user # Check if enough arguments have been passed if not builtins.howdy_args.arguments: print(_("Please add the ID of the model you want to remove as an argument")) print(_("For example:")) print("\n\thowdy remove 0\n") print(_("You can find the IDs by running:")) print("\n\thowdy list\n") sys.exit(1) # Check if the models file has been created yet if not os.path.exists(paths_factory.user_models_dir_path()): print(_("Face models have not been initialized yet, please run:")) print("\n\thowdy add\n") sys.exit(1) # Path to the models file enc_file = paths_factory.user_model_path(user) # Try to load the models file and abort if the user does not have it yet try: encodings = json.load(open(enc_file)) except FileNotFoundError: print(_("No face model known for the user {}, please run:").format(user)) print("\n\thowdy add\n") sys.exit(1) # Tracks if a encoding with that id has been found found = False # Get the ID from the cli arguments id = builtins.howdy_args.arguments[0] # Loop though all encodings and check if they match the argument for enc in encodings: if str(enc["id"]) == id: # Only ask the user if there's no -y flag if not builtins.howdy_args.y: # Double check with the user print(_('This will remove the model called "{label}" for {user}').format(label=enc["label"], user=user)) ans = input(_("Do you want to continue [y/N]: ")) # Abort if the answer isn't yes if (ans.lower() != "y"): print(_('\nInterpreting as a "NO", aborting')) sys.exit(1) # Add a padding empty line print() # Mark as found and print an enter found = True break # Abort if no matching id was found if not found: print(_("No model with ID {id} exists for {user}").format(id=id, user=user)) sys.exit(1) # Remove the entire file if this encoding is the only one if len(encodings) == 1: os.remove(paths_factory.user_model_path(user)) print(_("Removed last model, howdy disabled for user")) else: # A place holder to contain the encodings that will remain new_encodings = [] # Loop though all encodings and only add those that don't need to be removed for enc in encodings: if str(enc["id"]) != id: new_encodings.append(enc) # Save this new set to disk with open(enc_file, "w") as datafile: json.dump(new_encodings, datafile) print(_("Removed model {}").format(id)) ================================================ FILE: howdy/src/cli/set.py ================================================ # Set a config value # Import required modules import sys import os import builtins import fileinput import paths_factory from i18n import _ # Get the absolute filepath config_path = paths_factory.config_file_path() # Check if enough arguments have been passed if len(builtins.howdy_args.arguments) < 2: print(_("Please add a setting you would like to change and the value to set it to")) print(_("For example:")) print("\n\thowdy set certainty 3\n") sys.exit(1) # Get the name and value from the cli set_name = builtins.howdy_args.arguments[0] set_value = builtins.howdy_args.arguments[1] # Will be filled with the correctly config line to update found_line = "" # Loop through all lines in the config file for line in fileinput.input([config_path]): # Save the line if it starts with the requested config option if line.startswith(set_name + " "): found_line = line # If we don't have the line it is not in the config file if not found_line: print(_('Could not find a "{}" config option to set').format(set_name)) sys.exit(1) # Go through the file again and update the correct line for line in fileinput.input([config_path], inplace=1): print(line.replace(found_line, set_name + " = " + set_value + "\n"), end="") print(_("Config option updated")) ================================================ FILE: howdy/src/cli/snap.py ================================================ # Create a snapshot # Import required modules import os import configparser from datetime import timezone, datetime import snapshot import paths_factory from recorders.video_capture import VideoCapture from i18n import _ # Read the config config = configparser.ConfigParser() config.read(paths_factory.config_file_path()) # Start video capture video_capture = VideoCapture(config) # Read a frame to activate emitters video_capture.read_frame() # Read exposure and dark_thresholds from config to use in the main loop exposure = config.getint("video", "exposure", fallback=-1) dark_threshold = config.getfloat("video", "dark_threshold", fallback=60) # COllection of recorded frames frames = [] while True: # Grab a single frame of video frame, gsframe = video_capture.read_frame() # Add the frame to the list frames.append(frame) # Stop the loop if we have 4 frames if len(frames) >= 4: break # Generate a snapshot image from the frames file = snapshot.generate(frames, [ _("GENERATED SNAPSHOT"), _("Date: ") + datetime.now(timezone.utc).strftime("%Y/%m/%d %H:%M:%S UTC"), _("Dark threshold config: ") + str(config.getfloat("video", "dark_threshold", fallback=60.0)), _("Certainty config: ") + str(config.getfloat("video", "certainty", fallback=3.5)) ]) # Show the file location in console print(_("Generated snapshot saved as")) print(file) ================================================ FILE: howdy/src/cli/test.py ================================================ # Show a window with the video stream and testing information # Import required modules import configparser import builtins import os import json import sys import time import dlib import cv2 import numpy as np import paths_factory from i18n import _ from recorders.video_capture import VideoCapture # Read config from disk config = configparser.ConfigParser() config.read(paths_factory.config_file_path()) if config.get("video", "recording_plugin", fallback="opencv") != "opencv": print(_("Howdy has been configured to use a recorder which doesn't support the test command yet, aborting")) sys.exit(12) video_capture = VideoCapture(config) # Read config values to use in the main loop video_certainty = config.getfloat("video", "certainty", fallback=3.5) / 10 exposure = config.getint("video", "exposure", fallback=-1) dark_threshold = config.getfloat("video", "dark_threshold", fallback=60) # Let the user know what's up print(_(""" Opening a window with a test feed Press ctrl+C in this terminal to quit Click on the image to enable or disable slow mode """)) def mouse(event, x, y, flags, param): """Handle mouse events""" global slow_mode # Toggle slowmode on click if event == cv2.EVENT_LBUTTONDOWN: slow_mode = not slow_mode def print_text(line_number, text): """Print the status text by line number""" cv2.putText(overlay, text, (10, height - 10 - (10 * line_number)), cv2.FONT_HERSHEY_SIMPLEX, .3, (0, 255, 0), 0, cv2.LINE_AA) use_cnn = config.getboolean('core', 'use_cnn', fallback=False) if use_cnn: face_detector = dlib.cnn_face_detection_model_v1( paths_factory.mmod_human_face_detector_path() ) else: face_detector = dlib.get_frontal_face_detector() pose_predictor = dlib.shape_predictor(paths_factory.shape_predictor_5_face_landmarks_path()) face_encoder = dlib.face_recognition_model_v1(paths_factory.dlib_face_recognition_resnet_model_v1_path()) encodings = [] models = None try: user = builtins.howdy_user models = json.load(open(paths_factory.user_model_path(user))) for model in models: encodings += model["data"] except FileNotFoundError: pass clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) # Open the window and attach a a mouse listener cv2.namedWindow("Howdy Test") cv2.setMouseCallback("Howdy Test", mouse) # Enable a delay in the loop slow_mode = False # Count all frames ever total_frames = 0 # Count all frames per second sec_frames = 0 # Last secands FPS fps = 0 # The current second we're counting sec = int(time.time()) # recognition time rec_tm = 0 # Wrap everything in an keyboard interrupt handler try: while True: frame_tm = time.time() # Increment the frames total_frames += 1 sec_frames += 1 # Id we've entered a new second if sec != int(frame_tm): # Set the last seconds FPS fps = sec_frames # Set the new second and reset the counter sec = int(frame_tm) sec_frames = 0 # Grab a single frame of video orig_frame, frame = video_capture.read_frame() frame = clahe.apply(frame) # Make a frame to put overlays in overlay = frame.copy() overlay = cv2.cvtColor(overlay, cv2.COLOR_GRAY2BGR) # Fetch the frame height and width height, width = frame.shape[:2] # Create a histogram of the image with 8 values hist = cv2.calcHist([frame], [0], None, [8], [0, 256]) # All values combined for percentage calculation hist_total = int(sum(hist)[0]) # Fill with the overall containing percentage hist_perc = [] # Loop though all values to calculate a percentage and add it to the overlay for index, value in enumerate(hist): value_perc = float(value[0]) / hist_total * 100 hist_perc.append(value_perc) # Top left point, 10px margins p1 = (20 + (10 * index), 10) # Bottom right point makes the bar 10px thick, with an height of half the percentage p2 = (10 + (10 * index), int(value_perc / 2 + 10)) # Draw the bar in green cv2.rectangle(overlay, p1, p2, (0, 200, 0), thickness=cv2.FILLED) # Print the statis in the bottom left print_text(0, _("RESOLUTION: %dx%d") % (height, width)) print_text(1, _("FPS: %d") % (fps, )) print_text(2, _("FRAMES: %d") % (total_frames, )) print_text(3, _("RECOGNITION: %dms") % (round(rec_tm * 1000), )) # Show that slow mode is on, if it's on if slow_mode: cv2.putText(overlay, _("SLOW MODE"), (width - 66, height - 10), cv2.FONT_HERSHEY_SIMPLEX, .3, (0, 0, 255), 0, cv2.LINE_AA) # Ignore dark frames if hist_perc[0] > dark_threshold: # Show that this is an ignored frame in the top right cv2.putText(overlay, _("DARK FRAME"), (width - 68, 16), cv2.FONT_HERSHEY_SIMPLEX, .3, (0, 0, 255), 0, cv2.LINE_AA) else: # Show that this is an active frame cv2.putText(overlay, _("SCAN FRAME"), (width - 68, 16), cv2.FONT_HERSHEY_SIMPLEX, .3, (0, 255, 0), 0, cv2.LINE_AA) rec_tm = time.time() # Get the locations of all faces and their locations # Upsample it once face_locations = face_detector(frame, 1) rec_tm = time.time() - rec_tm # Loop though all faces and paint a circle around them for loc in face_locations: if use_cnn: loc = loc.rect # By default the circle around the face is red for no match color = (0, 0, 230) # Get the center X and Y from the rectangular points x = int((loc.right() - loc.left()) / 2) + loc.left() y = int((loc.bottom() - loc.top()) / 2) + loc.top() # Get the raduis from the with of the square r = (loc.right() - loc.left()) / 2 # Add 20% padding r = int(r + (r * 0.2)) # If we have models defined for the current user if models: # Get the encoding of the face in the frame face_landmark = pose_predictor(orig_frame, loc) face_encoding = np.array(face_encoder.compute_face_descriptor(orig_frame, face_landmark, 1)) # Match this found face against a known face matches = np.linalg.norm(encodings - face_encoding, axis=1) # Get best match match_index = np.argmin(matches) match = matches[match_index] # If a model matches if 0 < match < video_certainty: # Turn the circle green color = (0, 230, 0) # Print the name of the model next to the circle circle_text = "{} (certainty: {})".format(models[match_index]["label"], round(match * 10, 3)) cv2.putText(overlay, circle_text, (int(x + r / 3), y - r), cv2.FONT_HERSHEY_SIMPLEX, .3, (0, 255, 0), 0, cv2.LINE_AA) # If no approved matches, show red text else: cv2.putText(overlay, "no match", (int(x + r / 3), y - r), cv2.FONT_HERSHEY_SIMPLEX, .3, (0, 0, 255), 0, cv2.LINE_AA) # Draw the Circle in green cv2.circle(overlay, (x, y), r, color, 2) # Add the overlay to the frame with some transparency alpha = 0.65 frame = cv2.cvtColor(frame, cv2.COLOR_GRAY2BGR) cv2.addWeighted(overlay, alpha, frame, 1 - alpha, 0, frame) # Show the image in a window cv2.imshow("Howdy Test", frame) # Quit on any keypress if cv2.waitKey(1) != -1: raise KeyboardInterrupt() frame_time = time.time() - frame_tm # Delay the frame if slowmode is on if slow_mode: time.sleep(max([.5 - frame_time, 0.0])) if exposure != -1: # For a strange reason on some cameras (e.g. Lenoxo X1E) # setting manual exposure works only after a couple frames # are captured and even after a delay it does not # always work. Setting exposure at every frame is # reliable though. video_capture.internal.set(cv2.CAP_PROP_AUTO_EXPOSURE, 1.0) # 1 = Manual video_capture.internal.set(cv2.CAP_PROP_EXPOSURE, float(exposure)) # On ctrl+C except KeyboardInterrupt: # Let the user know we're stopping print(_("\nClosing window")) # Release handle to the webcam cv2.destroyAllWindows() ================================================ FILE: howdy/src/cli.py ================================================ # CLI directly called by running the howdy command # Import required modules import sys import os import pwd import getpass import argparse import builtins from i18n import _ # Try to get the original username (not "root") from shell sudo_user = os.environ.get("SUDO_USER") doas_user = os.environ.get("DOAS_USER") pkexec_uid = os.environ.get("PKEXEC_UID") pkexec_user = pwd.getpwuid(int(pkexec_uid))[0] if pkexec_uid else "" env_user = getpass.getuser() user = next((u for u in [sudo_user, doas_user, pkexec_user, env_user] if u), "") # If that fails, error out if user == "": print(_("Could not determine user, please use the --user flag")) sys.exit(1) # Basic command setup parser = argparse.ArgumentParser( description=_("Command line interface for Howdy face authentication."), formatter_class=argparse.RawDescriptionHelpFormatter, add_help=False, prog="howdy", usage="howdy [-U USER] [--plain] [-h] [-y] {command} [{arguments}...]".format(command=_("command"), arguments=_("arguments")), epilog=_("For support please visit\nhttps://github.com/boltgolt/howdy")) # Add an argument for the command parser.add_argument( "command", help=_("The command option to execute, can be one of the following: add, clear, config, disable, list, remove, snapshot, set, test or version."), metavar="command", choices=["add", "clear", "config", "disable", "list", "remove", "set", "snapshot", "test", "version"]) # Add an argument for the extra arguments of disable and remove parser.add_argument( "arguments", help=_("Optional arguments for the add, disable, remove and set commands."), nargs="*") # Add the user flag parser.add_argument( "-U", "--user", default=user, help=_("Set the user account to use.")) # Add the -y flag parser.add_argument( "-y", help=_("Skip all questions."), action="store_true") # Add the --plain flag parser.add_argument( "--plain", help=_("Print machine-friendly output."), action="store_true") # Overwrite the default help message so we can use a uppercase S parser.add_argument( "-h", "--help", action="help", default=argparse.SUPPRESS, help=_("Show this help message and exit.")) # If we only have 1 argument we print the help text if len(sys.argv) < 2: print(_("current active user: ") + user + "\n") parser.print_help() sys.exit(0) # Parse all arguments above args = parser.parse_args() # Save the args and user as builtins which can be accessed by the imports builtins.howdy_args = args builtins.howdy_user = args.user # Check if we have rootish rights # This is this far down the file so running the command for help is always possible if os.geteuid() != 0: print(_("Please run this command as root:\n")) print("\tsudo howdy " + " ".join(sys.argv[1:])) sys.exit(1) # Beyond this point the user can't change anymore, if we still have root as user we need to abort if args.user == "root": print(_("Can't run howdy commands as root, please run this command with the --user flag")) sys.exit(1) # Execute the right command if args.command == "add": import cli.add elif args.command == "clear": import cli.clear elif args.command == "config": import cli.config elif args.command == "disable": import cli.disable elif args.command == "list": import cli.list elif args.command == "remove": import cli.remove elif args.command == "set": import cli.set elif args.command == "snapshot": import cli.snap elif args.command == "test": import cli.test else: print("Howdy 3.0.0 BETA") ================================================ FILE: howdy/src/compare.py ================================================ # Compare incoming video with known faces # Running in a local python instance to get around PATH issues # Import time so we can start timing asap import time # Start timing timings = { "st": time.time() } # Import required modules import sys import os import json import configparser import dlib import cv2 from datetime import timezone, datetime import atexit import subprocess import snapshot import numpy as np import _thread as thread import paths_factory from recorders.video_capture import VideoCapture from i18n import _ def exit(code=None): """Exit while closing howdy-gtk properly""" global gtk_proc # Exit the auth ui process if there is one if "gtk_proc" in globals(): gtk_proc.terminate() # Exit compare if code is not None: sys.exit(code) def init_detector(lock): """Start face detector, encoder and predictor in a new thread""" global face_detector, pose_predictor, face_encoder # Test if at lest 1 of the data files is there and abort if it's not if not os.path.isfile(paths_factory.shape_predictor_5_face_landmarks_path()): print(_("Data files have not been downloaded, please run the following commands:")) print("\n\tcd " + paths_factory.dlib_data_dir_path()) print("\tsudo ./install.sh\n") lock.release() exit(1) # Use the CNN detector if enabled if use_cnn: face_detector = dlib.cnn_face_detection_model_v1(paths_factory.mmod_human_face_detector_path()) else: face_detector = dlib.get_frontal_face_detector() # Start the others regardless pose_predictor = dlib.shape_predictor(paths_factory.shape_predictor_5_face_landmarks_path()) face_encoder = dlib.face_recognition_model_v1(paths_factory.dlib_face_recognition_resnet_model_v1_path()) # Note the time it took to initialize detectors timings["ll"] = time.time() - timings["ll"] lock.release() def make_snapshot(type): """Generate snapshot after detection""" snapshot.generate(snapframes, [ type + _(" LOGIN"), _("Date: ") + datetime.now(timezone.utc).strftime("%Y/%m/%d %H:%M:%S UTC"), _("Scan time: ") + str(round(time.time() - timings["fr"], 2)) + "s", _("Frames: ") + str(frames) + " (" + str(round(frames / (time.time() - timings["fr"]), 2)) + "FPS)", _("Hostname: ") + os.uname().nodename, _("Best certainty value: ") + str(round(lowest_certainty * 10, 1)) ]) def send_to_ui(type, message): """Send message to the auth ui""" global gtk_proc # Only execute of the process started if "gtk_proc" in globals(): # Format message so the ui can parse it message = type + "=" + message + " \n" # Try to send the message to the auth ui, but it's okay if that fails try: if gtk_proc.poll() is None: # Make sure the gtk_proc is still running before write into the pipe gtk_proc.stdin.write(bytearray(message.encode("utf-8"))) gtk_proc.stdin.flush() except IOError: pass # Make sure we were given an username to test against if len(sys.argv) < 2: exit(12) # The username of the user being authenticated user = sys.argv[1] # The model file contents models = [] # Encoded face models encodings = [] # Amount of ignored 100% black frames black_tries = 0 # Amount of ignored dark frames dark_tries = 0 # Total amount of frames captured frames = 0 # Captured frames for snapshot capture snapframes = [] # Tracks the lowest certainty value in the loop lowest_certainty = 10 # Face recognition/detection instances face_detector = None pose_predictor = None face_encoder = None # Try to load the face model from the models folder try: models = json.load(open(paths_factory.user_model_path(user))) for model in models: encodings += model["data"] except FileNotFoundError: exit(10) # Check if the file contains a model if len(models) < 1: exit(10) # Read config from disk config = configparser.ConfigParser() config.read(paths_factory.config_file_path()) # Get all config values needed use_cnn = config.getboolean("core", "use_cnn", fallback=False) timeout = config.getint("video", "timeout", fallback=4) dark_threshold = config.getfloat("video", "dark_threshold", fallback=50.0) video_certainty = config.getfloat("video", "certainty", fallback=3.5) / 10 end_report = config.getboolean("debug", "end_report", fallback=False) save_failed = config.getboolean("snapshots", "save_failed", fallback=False) save_successful = config.getboolean("snapshots", "save_successful", fallback=False) gtk_stdout = config.getboolean("debug", "gtk_stdout", fallback=False) rotate = config.getint("video", "rotate", fallback=0) # Send the gtk output to the terminal if enabled in the config gtk_pipe = sys.stdout if gtk_stdout else subprocess.DEVNULL # Start the auth ui, register it to be always be closed on exit try: gtk_proc = subprocess.Popen(["howdy-gtk", "--start-auth-ui"], stdin=subprocess.PIPE, stdout=gtk_pipe, stderr=gtk_pipe) atexit.register(exit) except FileNotFoundError: pass # Write to the stdin to redraw ui send_to_ui("M", _("Starting up...")) # Save the time needed to start the script timings["in"] = time.time() - timings["st"] # Import face recognition, takes some time timings["ll"] = time.time() # Start threading and wait for init to finish lock = thread.allocate_lock() lock.acquire() thread.start_new_thread(init_detector, (lock, )) # Start video capture on the IR camera timings["ic"] = time.time() video_capture = VideoCapture(config) # Read exposure from config to use in the main loop exposure = config.getint("video", "exposure", fallback=-1) # Note the time it took to open the camera timings["ic"] = time.time() - timings["ic"] # wait for thread to finish lock.acquire() lock.release() del lock # Fetch the max frame height max_height = config.getfloat("video", "max_height", fallback=320.0) # Get the height of the image (which would be the width if screen is portrait oriented) height = video_capture.internal.get(cv2.CAP_PROP_FRAME_HEIGHT) or 1 if rotate == 2: height = video_capture.internal.get(cv2.CAP_PROP_FRAME_WIDTH) or 1 # Calculate the amount the image has to shrink scaling_factor = (max_height / height) or 1 # Fetch config settings out of the loop timeout = config.getint("video", "timeout", fallback=4) dark_threshold = config.getfloat("video", "dark_threshold", fallback=60) end_report = config.getboolean("debug", "end_report", fallback=False) # Initiate histogram equalization clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) # Let the ui know that we're ready send_to_ui("M", _("Identifying you...")) # Start the read loop frames = 0 valid_frames = 0 timings["fr"] = time.time() dark_running_total = 0 while True: # Increment the frame count every loop frames += 1 # Form a string to let the user know we're real busy ui_subtext = "Scanned " + str(valid_frames - dark_tries) + " frames" if (dark_tries > 1): ui_subtext += " (skipped " + str(dark_tries) + " dark frames)" # Show it in the ui as subtext send_to_ui("S", ui_subtext) # Stop if we've exceeded the time limit if time.time() - timings["fr"] > timeout: # Create a timeout snapshot if enabled if save_failed: make_snapshot(_("FAILED")) if dark_tries == valid_frames: print(_("All frames were too dark, please check dark_threshold in config")) print(_("Average darkness: {avg}, Threshold: {threshold}").format(avg=str(dark_running_total / max(1, valid_frames)), threshold=str(dark_threshold))) exit(13) else: exit(11) # Grab a single frame of video frame, gsframe = video_capture.read_frame() gsframe = clahe.apply(gsframe) # If snapshots have been turned on if save_failed or save_successful: # Start capturing frames for the snapshot if len(snapframes) < 3: snapframes.append(frame) # Create a histogram of the image with 8 values hist = cv2.calcHist([gsframe], [0], None, [8], [0, 256]) # All values combined for percentage calculation hist_total = np.sum(hist) # Calculate frame darkness darkness = (hist[0] / hist_total * 100) # If the image is fully black due to a bad camera read, # skip to the next frame if (hist_total == 0) or (darkness == 100): black_tries += 1 continue dark_running_total += darkness valid_frames += 1 # If the image exceeds darkness threshold due to subject distance, # skip to the next frame if (darkness > dark_threshold): dark_tries += 1 continue # If the height is too high if scaling_factor != 1: # Apply that factor to the frame frame = cv2.resize(frame, None, fx=scaling_factor, fy=scaling_factor, interpolation=cv2.INTER_AREA) gsframe = cv2.resize(gsframe, None, fx=scaling_factor, fy=scaling_factor, interpolation=cv2.INTER_AREA) # If camera is configured to rotate = 1, check portrait in addition to landscape if rotate == 1: if frames % 3 == 1: frame = cv2.rotate(frame, cv2.ROTATE_90_COUNTERCLOCKWISE) gsframe = cv2.rotate(gsframe, cv2.ROTATE_90_COUNTERCLOCKWISE) if frames % 3 == 2: frame = cv2.rotate(frame, cv2.ROTATE_90_CLOCKWISE) gsframe = cv2.rotate(gsframe, cv2.ROTATE_90_CLOCKWISE) # If camera is configured to rotate = 2, check portrait orientation elif rotate == 2: if frames % 2 == 0: frame = cv2.rotate(frame, cv2.ROTATE_90_COUNTERCLOCKWISE) gsframe = cv2.rotate(gsframe, cv2.ROTATE_90_COUNTERCLOCKWISE) else: frame = cv2.rotate(frame, cv2.ROTATE_90_CLOCKWISE) gsframe = cv2.rotate(gsframe, cv2.ROTATE_90_CLOCKWISE) # Get all faces from that frame as encodings # Upsamples 1 time face_locations = face_detector(gsframe, 1) # Loop through each face for fl in face_locations: if use_cnn: fl = fl.rect # Fetch the faces in the image face_landmark = pose_predictor(frame, fl) face_encoding = np.array(face_encoder.compute_face_descriptor(frame, face_landmark, 1)) # Match this found face against a known face matches = np.linalg.norm(encodings - face_encoding, axis=1) # Get best match match_index = np.argmin(matches) match = matches[match_index] # Update certainty if we have a new low if lowest_certainty > match: lowest_certainty = match # Check if a match that's confident enough if 0 < match < video_certainty: timings["tt"] = time.time() - timings["st"] timings["fl"] = time.time() - timings["fr"] # If set to true in the config, print debug text if end_report: def print_timing(label, k): """Helper function to print a timing from the list""" print(" %s: %dms" % (label, round(timings[k] * 1000))) # Print a nice timing report print(_("Time spent")) print_timing(_("Starting up"), "in") print(_(" Open cam + load libs: %dms") % (round(max(timings["ll"], timings["ic"]) * 1000, ))) print_timing(_(" Opening the camera"), "ic") print_timing(_(" Importing recognition libs"), "ll") print_timing(_("Searching for known face"), "fl") print_timing(_("Total time"), "tt") print(_("\nResolution")) width = video_capture.fw or 1 print(_(" Native: %dx%d") % (height, width)) # Save the new size for diagnostics scale_height, scale_width = frame.shape[:2] print(_(" Used: %dx%d") % (scale_height, scale_width)) # Show the total number of frames and calculate the FPS by dividing it by the total scan time print(_("\nFrames searched: %d (%.2f fps)") % (frames, frames / timings["fl"])) print(_("Black frames ignored: %d ") % (black_tries, )) print(_("Dark frames ignored: %d ") % (dark_tries, )) print(_("Certainty of winning frame: %.3f") % (match * 10, )) print(_("Winning model: %d (\"%s\")") % (match_index, models[match_index]["label"])) # Make snapshot if enabled if save_successful: make_snapshot(_("SUCCESSFUL")) # Run rubberstamps if enabled if config.getboolean("rubberstamps", "enabled", fallback=False): import rubberstamps send_to_ui("S", "") if "gtk_proc" not in vars(): gtk_proc = None rubberstamps.execute(config, gtk_proc, { "video_capture": video_capture, "face_detector": face_detector, "pose_predictor": pose_predictor, "clahe": clahe }) # End peacefully exit(0) if exposure != -1: # For a strange reason on some cameras (e.g. Lenoxo X1E) setting manual exposure works only after a couple frames # are captured and even after a delay it does not always work. Setting exposure at every frame is reliable though. video_capture.internal.set(cv2.CAP_PROP_AUTO_EXPOSURE, 1.0) # 1 = Manual video_capture.internal.set(cv2.CAP_PROP_EXPOSURE, float(exposure)) ================================================ FILE: howdy/src/config.ini ================================================ # Howdy config file # Press CTRL + X to save in the nano editor [core] # Print that face detection is being attempted detection_notice = false # Print that face detection has timed out timeout_notice = true # Do not print anything when a face verification succeeds no_confirmation = false # When a user without a known face model tries to use this script, don't # show an error but fail silently suppress_unknown = false # Disable Howdy in remote shells abort_if_ssh = true # Disable Howdy if lid is closed abort_if_lid_closed = true # Disable howdy in the PAM # The howdy command will still function disabled = false # Use CNN instead of HOG # CNN model is much more accurate than the HOG based model, but takes much more # power to run, and is meant to be executed on a GPU to attain reasonable speed. use_cnn = false # Set a workaround to do face and password authentication at the same time # off user will have to press enter themselves after a Howdy timeout # input will send an enter keypress to stop the password prompt # native will stop the prompt at PAM level (can lead to instability!) workaround = off [video] # The certainty of the detected face belonging to the user of the account # On a scale from 1 to 10, values above 5 are not recommended # The lower, the more accurate certainty = 3.5 # The number of seconds to search before timing out timeout = 4 # The path of the device to capture frames from # Video devices are usually found in /dev/v4l/by-path/ device_path = none # Print a warning if the the video device is not found warn_no_device = true # Scale down the video feed to this maximum height # Speeds up face recognition but can make it less precise max_height = 320 # Set the camera input profile to this width and height # The largest profile will be used if set to -1 # Automatically ignored if not a valid profile frame_width = -1 frame_height = -1 # Because of flashing IR emitters, some frames can be completely unlit # Skip the frame if the lowest 1/8 of the histogram is above this percentage # of the total # The lower this setting is, the more dark frames are ignored dark_threshold = 60 # The recorder to use. Can be either opencv (default), ffmpeg or pyv4l2. # Switching from the default opencv to ffmpeg can help with grayscale issues. recording_plugin = opencv # Video format used by ffmpeg. Options include vfwcap or v4l2. # FFMPEG only. device_format = v4l2 # Force the use of Motion JPEG when decoding frames, fixes issues with YUYV # raw frame decoding. # OPENCV only. force_mjpeg = false # Specify exposure value explicitly. This disables autoexposure. # Use qv4l2 to determine an appropriate value. # OPENCV only. exposure = -1 # Specify frame rate of the capture device. # Some IR emitters will not function properly at the default framerate. # Use qv4l2 to determine an appropriate value. # OPENCV only. device_fps = -1 # Rotate captured frames so faces are upright. # 0 Check landscape orientation only # 1 Check both landscape and portrait orientation # 2 Check portrait orientation only rotate = 0 [snapshots] # Capture snapshots of failed login attempts and save them to disk with metadata # Snapshots are saved to /var/log/howdy/snapshots save_failed = false # Do the same as the option above but for successful attempts save_successful = false [rubberstamps] # Enable specific extra checks after the user has been recognised enabled = false # What type of stamps to run and with what options. The type, timeout and # failure mode are required. One line per stamp. Rule syntax: # stamptype timeout (failsafe | faildeadly) [extra_argument=value] stamp_rules = nod 5s failsafe min_distance=12 [debug] # Show a short but detailed diagnostic report in console # Enabling this can cause some UI apps to fail, only enable it to debug end_report = false # More verbose logging from the rubberstamps system verbose_stamps = false # Pass output of the GTK auth window to the terminal gtk_stdout = false ================================================ FILE: howdy/src/dlib-data/.gitignore ================================================ *.dat *.dat.bz2 ================================================ FILE: howdy/src/dlib-data/Readme.md ================================================ Download and unpack `dlib` data files from https://github.com/davisking/dlib-models repository: ``` shell wget https://github.com/davisking/dlib-models/raw/master/dlib_face_recognition_resnet_model_v1.dat.bz2 wget https://github.com/davisking/dlib-models/raw/master/mmod_human_face_detector.dat.bz2 wget https://github.com/davisking/dlib-models/raw/master/shape_predictor_5_face_landmarks.dat.bz2 bunzip *bz2 ``` ================================================ FILE: howdy/src/dlib-data/install.sh ================================================ #!/bin/bash echo "Downloading 3 required data files..." # Check if wget is installed if hash wget;then # Check if wget supports the option to only show the progress bar wget --help | grep -q "\--show-progress" && \ _PROGRESS_OPT="-q --show-progress" || _PROGRESS_OPT="" # Download the archives wget $_PROGRESS_OPT --tries 5 https://github.com/davisking/dlib-models/raw/master/dlib_face_recognition_resnet_model_v1.dat.bz2 wget $_PROGRESS_OPT --tries 5 https://github.com/davisking/dlib-models/raw/master/mmod_human_face_detector.dat.bz2 wget $_PROGRESS_OPT --tries 5 https://github.com/davisking/dlib-models/raw/master/shape_predictor_5_face_landmarks.dat.bz2 # Otherwise fall back on curl else curl --location --retry 5 --output dlib_face_recognition_resnet_model_v1.dat.bz2 https://github.com/davisking/dlib-models/raw/master/dlib_face_recognition_resnet_model_v1.dat.bz2 curl --location --retry 5 --output mmod_human_face_detector.dat.bz2 https://github.com/davisking/dlib-models/raw/master/mmod_human_face_detector.dat.bz2 curl --location --retry 5 --output shape_predictor_5_face_landmarks.dat.bz2 https://github.com/davisking/dlib-models/raw/master/shape_predictor_5_face_landmarks.dat.bz2 fi # Uncompress the data files and delete the original archive echo " " echo "Unpacking..." bzip2 -d -f *.bz2 ================================================ FILE: howdy/src/i18n.py ================================================ # Support file for translations # Import modules import gettext import os # Get the right translation based on locale, falling back to base if none found translation = gettext.translation("core", localedir=os.path.join(os.path.dirname(__file__), 'locales'), fallback=True) translation.install() # Export translation function as _ _ = translation.gettext ================================================ FILE: howdy/src/meson.build ================================================ if meson.is_subproject() project('howdy', 'cpp', license: 'MIT', version: 'beta', meson_version: '>= 0.64.0') endif datadir = get_option('prefix') / get_option('datadir') / 'howdy' py_conf = configuration_data(paths_dict) py_conf.set('data_dir', datadir) py = import('python').find_installation(paths_dict.get('python_path')) py.dependency() py_paths = configure_file( input: 'paths.py.in', output: 'paths.py', configuration: py_conf, ) py_sources = [ 'cli/__init__.py', 'cli/add.py', 'cli/clear.py', 'cli/config.py', 'cli/disable.py', 'cli/list.py', 'cli/remove.py', 'cli/set.py', 'cli/snap.py', 'cli/test.py', 'cli.py', 'compare.py', 'i18n.py', 'paths_factory.py', 'recorders/__init__.py', 'recorders/ffmpeg_reader.py', 'recorders/pyv4l2_reader.py', 'recorders/v4l2.py', 'recorders/video_capture.py', 'rubberstamps/__init__.py', 'rubberstamps/hotkey.py', 'rubberstamps/nod.py', 'snapshot.py', py_paths, ] # Include PAM module if get_option('install_in_site_packages') pysourcesinstalldir = join_paths(py.get_install_dir(), 'howdy') else pysourcesinstalldir = get_option('py_sources_dir') != '' ? get_option('py_sources_dir') / 'howdy' : join_paths(get_option('prefix'), get_option('libdir'), 'howdy') endif pam_module_conf_data = configuration_data(paths_dict) pam_module_conf_data.set('compare_script_path', join_paths(pysourcesinstalldir, 'compare.py')) pam_module_conf_data.set('config_file_path', config_path) subdir('pam') if get_option('install_pam_config') # pamdir is inherited from the pam subproject pam_config = configure_file( input: 'pam-config/howdy.in', output: 'pam-config', configuration: {'pamdir': pamdir} ) install_data( pam_config, install_dir: get_option('prefix') / get_option('datadir') / 'pam-configs', install_mode: 'rwxr-xr-x', install_tag: 'pam', rename: 'howdy', ) endif if get_option('install_in_site_packages') py.install_sources( py_sources, subdir: 'howdy', preserve_path: true, install_tag: 'py_sources', ) else install_data( py_sources, preserve_path: true, install_dir: pysourcesinstalldir, install_mode: 'r--r--r--', install_tag: 'py_sources', ) endif install_data('logo.png', install_tag: 'meta') autocomplete = configure_file( input: 'autocomplete/howdy.in', output: 'autocomplete', configuration: configuration_data({ 'config_path': config_path }) ) install_data( autocomplete, install_dir: join_paths(get_option('prefix'), get_option('datadir'), 'bash-completion', 'completions'), install_mode: 'rwxr--r--', install_tag: 'bash_completion', rename: 'howdy', ) fs = import('fs') if not fs.exists(config_path) install_data('config.ini', install_dir: confdir, install_mode: 'rwxr--r--', install_tag: 'config') endif install_data('dlib-data/install.sh', 'dlib-data/Readme.md', install_dir: dlibdatadir, install_mode: 'rwxr--r--') install_man('../howdy.1') # if get_option('fetch_dlib_data') # downloader = find_program('wget') # bunzip2 = find_program('bunzip2') # links = [ # 'https://github.com/davisking/dlib-models/raw/master/dlib_face_recognition_resnet_model_v1.dat.bz2', # 'https://github.com/davisking/dlib-models/raw/master/mmod_human_face_detector.dat.bz2', # 'https://github.com/davisking/dlib-models/raw/master/shape_predictor_5_face_landmarks.dat.bz2' # ] # archived_model_files = [ # 'dlib_face_recognition_resnet_model_v1.dat.bz2', # 'shape_predictor_5_face_landmarks.dat.bz2', # 'mmod_human_face_detector.dat.bz2' # ] # download = run_command( # 'download', # links, # output: archived_model_files, # command: [downloader, '-O', '@OUTPUT@', '@INPUT@'] # ) # model_files = [ # 'dlib_face_recognition_resnet_model_v1.dat', # 'shape_predictor_5_face_landmarks.dat', # 'mmod_human_face_detector.dat' # ] # models = custom_target( # 'models', # input: archived_model_files, # output: model_files, # command: [bunzip2, '-k', '@INPUT@'], # ) # install_data( # model_files, # install_dir: join_paths(get_option('prefix'), get_option('libdir'), 'dlib_models'), # ) # endif cli_path = join_paths(pysourcesinstalldir, 'cli.py') conf_data = configuration_data({ 'script_path': cli_path, 'python_path': py.full_path() }) bin_name = 'howdy' bin = configure_file( input: 'bin/howdy.in', output: bin_name, configuration: conf_data ) install_data( bin, install_mode: 'rwxr-xr-x', install_dir: get_option('bindir'), install_tag: 'bin', ) ================================================ FILE: howdy/src/pam/.gitignore ================================================ subprojects/inih/ ================================================ FILE: howdy/src/pam/README.md ================================================ # Howdy PAM module ## Requirements This module depends on `INIReader` and `libevdev`. They can be installed with these packages: ``` Arch Linux - libinih libevdev Debian - libinih-dev libevdev-dev Fedora - inih-devel libevdev-devel OpenSUSE - inih libevdev-devel ``` If your distribution doesn't provide `INIReader`, it will be automatically pulled from git at the subproject's pinned version. ## Build ``` sh meson setup build ninja -C build # or meson compile -C build ``` ## Install ``` sh meson install -C build ``` Add the following line to your PAM configuration (/etc/pam.d/your-service): ``` pam auth sufficient pam_howdy.so ``` ================================================ FILE: howdy/src/pam/enter_device.cc ================================================ #include "enter_device.hh" #include #include #include EnterDevice::EnterDevice() : raw_device(libevdev_new(), &libevdev_free), raw_uinput_device(nullptr, &libevdev_uinput_destroy) { auto *dev_ptr = raw_device.get(); libevdev_set_name(dev_ptr, "enter device"); libevdev_enable_event_type(dev_ptr, EV_KEY); libevdev_enable_event_code(dev_ptr, EV_KEY, KEY_ENTER, nullptr); int err; struct libevdev_uinput *uinput_dev_ptr; err = libevdev_uinput_create_from_device(dev_ptr, LIBEVDEV_UINPUT_OPEN_MANAGED, &uinput_dev_ptr); if (err != 0) { throw std::runtime_error(std::string("Failed to create device: ") + strerror(-err)); } raw_uinput_device.reset(uinput_dev_ptr); }; void EnterDevice::send_enter_press() const { auto *uinput_dev_ptr = raw_uinput_device.get(); int err; err = libevdev_uinput_write_event(uinput_dev_ptr, EV_KEY, KEY_ENTER, 1); if (err != 0) { throw std::runtime_error(std::string("Failed to write event: ") + strerror(-err)); } err = libevdev_uinput_write_event(uinput_dev_ptr, EV_KEY, KEY_ENTER, 0); if (err != 0) { throw std::runtime_error(std::string("Failed to write event: ") + strerror(-err)); } err = libevdev_uinput_write_event(uinput_dev_ptr, EV_SYN, SYN_REPORT, 0); if (err != 0) { throw std::runtime_error(std::string("Failed to write event: ") + strerror(-err)); } } ================================================ FILE: howdy/src/pam/enter_device.hh ================================================ #ifndef ENTER_DEVICE_H_ #define ENTER_DEVICE_H_ #include #include #include class EnterDevice { std::unique_ptr raw_device; std::unique_ptr raw_uinput_device; public: EnterDevice(); void send_enter_press() const; ~EnterDevice() = default; }; #endif // ENTER_DEVICE_H ================================================ FILE: howdy/src/pam/main.cc ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "enter_device.hh" #include "main.hh" #include "optional_task.hh" #include const auto DEFAULT_TIMEOUT = std::chrono::duration(100); const auto MAX_RETRIES = 5; #define S(msg) gettext(msg) /** * Inspect the status code returned by the compare process * @param status The status code * @param conv_function The PAM conversation function * @return A PAM return code */ auto howdy_error(int status, const std::function &conv_function) -> int { // If the process has exited if (WIFEXITED(status)) { // Get the status code returned status = WEXITSTATUS(status); switch (status) { case CompareError::NO_FACE_MODEL: syslog(LOG_NOTICE, "Failure, no face model known"); break; case CompareError::TIMEOUT_REACHED: conv_function(PAM_ERROR_MSG, S("Failure, timeout reached")); syslog(LOG_ERR, "Failure, timeout reached"); break; case CompareError::ABORT: syslog(LOG_ERR, "Failure, general abort"); break; case CompareError::TOO_DARK: conv_function(PAM_ERROR_MSG, S("Face detection image too dark")); syslog(LOG_ERR, "Failure, image too dark"); break; case CompareError::INVALID_DEVICE: syslog(LOG_ERR, "Failure, not possible to open camera at configured path"); break; default: conv_function(PAM_ERROR_MSG, std::string(S("Unknown error: ") + status).c_str()); syslog(LOG_ERR, "Failure, unknown error %d", status); } } else if (WIFSIGNALED(status)) { // We get the signal status = WTERMSIG(status); syslog(LOG_ERR, "Child killed by signal %s (%d)", strsignal(status), status); } // As this function is only called for error status codes, signal an error to // PAM return PAM_AUTH_ERR; } /** * Format the success message if the status is successful or log the error in * the other case * @param username Username * @param status Status code * @param config INI configuration * @param conv_function PAM conversation function * @return Returns the conversation function return code */ auto howdy_status(char *username, int status, const INIReader &config, const std::function &conv_function) -> int { if (status != EXIT_SUCCESS) { return howdy_error(status, conv_function); } if (!config.GetBoolean("core", "no_confirmation", true)) { // Construct confirmation text from i18n string std::string confirm_text(S("Identified face as {}")); std::string identify_msg = confirm_text.replace(confirm_text.find("{}"), 2, std::string(username)); conv_function(PAM_TEXT_INFO, identify_msg.c_str()); } syslog(LOG_INFO, "Login approved"); return PAM_SUCCESS; } /** * Check if Howdy should be enabled according to the configuration and the * environment. * @param config INI configuration * @param username Username * @return Returns PAM_AUTHINFO_UNAVAIL if it shouldn't be enabled, * PAM_SUCCESS otherwise */ auto check_enabled(const INIReader &config, const char *username) -> int { // Stop executing if Howdy has been disabled in the config if (config.GetBoolean("core", "disabled", false)) { syslog(LOG_INFO, "Skipped authentication, Howdy is disabled"); return PAM_AUTHINFO_UNAVAIL; } // Stop if we're in a remote shell and configured to exit if (config.GetBoolean("core", "abort_if_ssh", true)) { if (checkenv("SSH_CONNECTION") || checkenv("SSH_CLIENT") || checkenv("SSH_TTY") || checkenv("SSHD_OPTS")) { syslog(LOG_INFO, "Skipped authentication, SSH session detected"); return PAM_AUTHINFO_UNAVAIL; } } // Try to detect the laptop lid state and stop if it's closed if (config.GetBoolean("core", "abort_if_lid_closed", true)) { glob_t glob_result; // Get any files containing lid state int return_value = glob("/proc/acpi/button/lid/*/state", 0, nullptr, &glob_result); if (return_value != 0) { syslog(LOG_ERR, "Failed to read files from glob: %d", return_value); if (errno != 0) { syslog(LOG_ERR, "Underlying error: %s (%d)", strerror(errno), errno); } } else { for (size_t i = 0; i < glob_result.gl_pathc; i++) { std::ifstream file(std::string(glob_result.gl_pathv[i])); std::string lid_state; std::getline(file, lid_state, static_cast(file.eof())); if (lid_state.find("closed") != std::string::npos) { globfree(&glob_result); syslog(LOG_INFO, "Skipped authentication, closed lid detected"); return PAM_AUTHINFO_UNAVAIL; } } } globfree(&glob_result); } // pre-check if this user has face model file auto model_path = std::string(USER_MODELS_DIR) + "/" + username + ".dat"; struct stat stat_; if (stat(model_path.c_str(), &stat_) != 0) { return PAM_AUTHINFO_UNAVAIL; } return PAM_SUCCESS; } /** * The main function, runs the identification and authentication * @param pamh The handle to interface directly with PAM * @param flags Flags passed on to us by PAM, XORed * @param argc Amount of rules in the PAM config (disregarded) * @param argv Options defined in the PAM config * @param ask_auth_tok True if we should ask for a password too * @return Returns a PAM return code */ auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, bool ask_auth_tok) -> int { INIReader config(CONFIG_FILE_PATH); openlog("pam_howdy", 0, LOG_AUTHPRIV); // Error out if we could not read the config file if (config.ParseError() != 0) { syslog(LOG_ERR, "Failed to parse the configuration file: %d", config.ParseError()); return PAM_SYSTEM_ERR; } // Will contain the responses from PAM functions int pam_res = PAM_IGNORE; // Get the username from PAM, needed to match correct face model char *username = nullptr; pam_res = pam_get_user(pamh, const_cast(&username), nullptr); if (pam_res != PAM_SUCCESS) { syslog(LOG_ERR, "Failed to get username"); return pam_res; } // Check if we should continue pam_res = check_enabled(config, username); if (pam_res != PAM_SUCCESS) { return pam_res; } Workaround workaround = get_workaround(config.GetString("core", "workaround", "input")); // Will contain PAM conversation structure struct pam_conv *conv = nullptr; const void **conv_ptr = const_cast(reinterpret_cast(&conv)); // Retrieve the PAM conversation structure pam_res = pam_get_item(pamh, PAM_CONV, conv_ptr); if (pam_res != PAM_SUCCESS) { syslog(LOG_ERR, "Failed to acquire conversation"); return pam_res; } // Wrap the PAM conversation function in our own, easier function auto conv_function = [conv](int msg_type, const char *msg_str) { const struct pam_message msg = {.msg_style = msg_type, .msg = msg_str}; const struct pam_message *msgp = &msg; struct pam_response res = {}; struct pam_response *resp = &res; return conv->conv(1, &msgp, &resp, conv->appdata_ptr); }; // Initialize gettext setlocale(LC_ALL, ""); bindtextdomain(GETTEXT_PACKAGE, LOCALEDIR); textdomain(GETTEXT_PACKAGE); if (config.GetBoolean("core", "detection_notice", true)) { if ((conv_function(PAM_TEXT_INFO, S("Attempting facial authentication"))) != PAM_SUCCESS) { syslog(LOG_ERR, "Failed to send detection notice"); } } const char *const args[] = {PYTHON_EXECUTABLE_PATH, // NOLINT COMPARE_PROCESS_PATH, username, nullptr}; pid_t child_pid; // Start the python subprocess if (posix_spawnp(&child_pid, PYTHON_EXECUTABLE_PATH, nullptr, nullptr, const_cast(args), nullptr) != 0) { syslog(LOG_ERR, "Can't spawn the howdy process: %s (%d)", strerror(errno), errno); return PAM_SYSTEM_ERR; } // NOTE: We should replace mutex and condition_variable by atomic wait, but // it's too recent (C++20) std::mutex mutx; std::condition_variable convar; ConfirmationType confirmation_type(ConfirmationType::Unset); // This task wait for the status of the python subprocess (we don't want a // zombie process) optional_task child_task([&] { int status; waitpid(child_pid, &status, 0); { std::unique_lock lock(mutx); if (confirmation_type == ConfirmationType::Unset) { confirmation_type = ConfirmationType::Howdy; } } convar.notify_one(); return status; }); child_task.activate(); // This task waits for the password input (if the workaround wants it) optional_task> pass_task([&] { char *auth_tok_ptr = nullptr; int pam_res = pam_get_authtok( pamh, PAM_AUTHTOK, const_cast(&auth_tok_ptr), nullptr); { std::unique_lock lock(mutx); if (confirmation_type == ConfirmationType::Unset) { confirmation_type = ConfirmationType::Pam; } } convar.notify_one(); return std::tuple(pam_res, auth_tok_ptr); }); auto ask_pass = ask_auth_tok && workaround != Workaround::Off; // We ask for the password if the function requires it and if a workaround is // set if (ask_pass) { pass_task.activate(); } // Wait for the end either of the child or the password input { std::unique_lock lock(mutx); convar.wait(lock, [&] { return confirmation_type != ConfirmationType::Unset; }); } // The password has been entered or an error has occurred if (confirmation_type == ConfirmationType::Pam) { // We kill the child because we don't need its result kill(child_pid, SIGTERM); child_task.stop(false); // We just wait for the thread to stop since it's this one which sent us the // confirmation type pass_task.stop(false); char *password = nullptr; std::tie(pam_res, password) = pass_task.get(); if (pam_res != PAM_SUCCESS) { return pam_res; } // The password has been entered, we are passing it to PAM stack return PAM_IGNORE; } // The compare process has finished its execution child_task.stop(false); // Get python process status code int status = child_task.get(); // If python process ran into a timeout // Do not send enter presses or terminate the PAM function, as the user might // still be typing their password if (WIFEXITED(status) && WEXITSTATUS(status) != EXIT_SUCCESS && ask_pass) { // Wait for the password to be typed pass_task.stop(false); char *password = nullptr; std::tie(pam_res, password) = pass_task.get(); if (pam_res != PAM_SUCCESS) { return howdy_status(username, status, config, conv_function); } // The password has been entered, we are passing it to PAM stack return PAM_IGNORE; } // We want to stop the password prompt, either by canceling the thread when // workaround is set to "native", or by emulating "Enter" input with // "input" // UNSAFE: We cancel the thread using pthread, pam_get_authtok seems to be // a cancellation point if (workaround == Workaround::Native) { pass_task.stop(true); } else if (workaround == Workaround::Input) { // We check if we have the right permissions on /dev/uinput if (euidaccess("/dev/uinput", W_OK | R_OK) != 0) { syslog(LOG_WARNING, "Insufficient permissions to create the fake device"); conv_function(PAM_ERROR_MSG, S("Insufficient permissions to send Enter " "press, waiting for user to press it instead")); } else { try { EnterDevice enter_device; int retries; // We try to send it enter_device.send_enter_press(); for (retries = 0; retries < MAX_RETRIES && pass_task.wait(DEFAULT_TIMEOUT) == std::future_status::timeout; retries++) { enter_device.send_enter_press(); } if (retries == MAX_RETRIES) { syslog(LOG_WARNING, "Failed to send enter input before the retries limit"); conv_function(PAM_ERROR_MSG, S("Failed to send Enter press, waiting " "for user to press it instead")); } } catch (std::runtime_error &err) { syslog(LOG_WARNING, "Failed to send enter input: %s", err.what()); conv_function(PAM_ERROR_MSG, S("Failed to send Enter press, waiting " "for user to press it instead")); } } // We stop the thread (will block until the enter key is pressed if the // input wasn't focused or if the uinput device failed to send keypress) pass_task.stop(false); } return howdy_status(username, status, config, conv_function); } // Called by PAM when a user needs to be authenticated, for example by running // the sudo command PAM_EXTERN auto pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv) -> int { return identify(pamh, flags, argc, argv, true); } // Called by PAM when a session is started, such as by the su command PAM_EXTERN auto pam_sm_open_session(pam_handle_t *pamh, int flags, int argc, const char **argv) -> int { return identify(pamh, flags, argc, argv, false); } // The functions below are required by PAM, but not needed in this module PAM_EXTERN auto pam_sm_acct_mgmt(pam_handle_t *pamh, int flags, int argc, const char **argv) -> int { return PAM_IGNORE; } PAM_EXTERN auto pam_sm_close_session(pam_handle_t *pamh, int flags, int argc, const char **argv) -> int { return PAM_IGNORE; } PAM_EXTERN auto pam_sm_chauthtok(pam_handle_t *pamh, int flags, int argc, const char **argv) -> int { return PAM_IGNORE; } PAM_EXTERN auto pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char **argv) -> int { return PAM_IGNORE; } ================================================ FILE: howdy/src/pam/main.hh ================================================ #ifndef MAIN_H_ #define MAIN_H_ #include #include #include #include enum class ConfirmationType : std::uint8_t { Unset, Howdy, Pam }; enum class Workaround : std::uint8_t { Off, Input, Native }; // Exit status codes returned by the compare process enum CompareError : std::uint8_t { NO_FACE_MODEL = 10, TIMEOUT_REACHED = 11, ABORT = 12, TOO_DARK = 13, INVALID_DEVICE = 14, RUBBERSTAMP = 15 }; inline auto get_workaround(const std::string &workaround) -> Workaround { if (workaround == "input") { return Workaround::Input; } if (workaround == "native") { return Workaround::Native; } return Workaround::Off; } /** * Check if an environment variable exists either in the environ array or using * getenv. * @param name The name of the environment variable. * @return The value of the environment variable or nullptr if it doesn't exist * or environ is nullptr. * @note This function was created because `getenv` wasn't working properly in * some contexts (like sudo). */ auto checkenv(const char *name) -> bool { if (std::getenv(name) != nullptr) { return true; } auto len = strlen(name); for (char **env = environ; *env != nullptr; env++) { if (strncmp(*env, name, len) == 0) { return true; } } return false; } #endif // MAIN_H_ ================================================ FILE: howdy/src/pam/meson.build ================================================ inih_cpp = dependency('INIReader', fallback: ['inih', 'INIReader_dep']) libevdev = dependency('libevdev') libpam = meson.get_compiler('cpp').find_library('pam') threads = dependency('threads') # Translations subdir('po') # Paths paths_h = configure_file( input: 'paths.hh.in', output: 'paths.hh', configuration: pam_module_conf_data ) pamdir = get_option('pam_dir') != '' ? get_option('pam_dir') : join_paths(get_option('prefix'), get_option('libdir'), 'security') shared_library( 'pam_howdy', 'main.cc', 'enter_device.cc', dependencies: [ libpam, inih_cpp, threads, libevdev, ], link_depends: [ paths_h, ], install: true, install_dir: pamdir, install_tag: 'pam_module', name_prefix: '' ) ================================================ FILE: howdy/src/pam/optional_task.hh ================================================ #ifndef OPTIONAL_TASK_H_ #define OPTIONAL_TASK_H_ #include #include #include #include // A task executed only if activated. template class optional_task { std::thread thread; std::packaged_task task; std::future future; bool spawned{false}; bool is_active{false}; public: explicit optional_task(std::function func); void activate(); template auto wait(std::chrono::duration dur) -> std::future_status; auto get() -> T; void stop(bool force); ~optional_task(); }; template optional_task::optional_task(std::function func) : task(std::packaged_task(std::move(func))), future(task.get_future()) {} // Create a new thread and launch the task on it. template void optional_task::activate() { thread = std::thread(std::move(task)); spawned = true; is_active = true; } // Wait for `dur` time and return a `future` status. template template auto optional_task::wait(std::chrono::duration dur) -> std::future_status { return future.wait_for(dur); } // Get the value. // WARNING: The function should be run only if the task has successfully been // stopped. template auto optional_task::get() -> T { assert(!is_active && spawned); return future.get(); } // Stop the thread: // - if `force` is `false`, by joining the thread. // - if `force` is `true`, by cancelling the thread using `pthread_cancel`. // WARNING: This function should be used with extreme caution when `force` is // set to `true`. template void optional_task::stop(bool force) { if (!(is_active && thread.joinable()) && spawned) { is_active = false; return; } // We use pthread to cancel the thread if (force) { auto native_hd = thread.native_handle(); pthread_cancel(native_hd); } thread.join(); is_active = false; } template optional_task::~optional_task() { if (is_active && spawned) { stop(false); } } #endif // OPTIONAL_TASK_H_ ================================================ FILE: howdy/src/pam/paths.hh.in ================================================ const auto COMPARE_PROCESS_PATH = "@compare_script_path@"; const auto CONFIG_FILE_PATH = "@config_file_path@"; const auto USER_MODELS_DIR = "@user_models_dir@"; const auto PYTHON_EXECUTABLE_PATH = "@python_path@"; ================================================ FILE: howdy/src/pam/po/LINGUAS ================================================ ================================================ FILE: howdy/src/pam/po/POTFILES ================================================ main.cc ================================================ FILE: howdy/src/pam/po/meson.build ================================================ i18n = import('i18n') # define GETTEXT_PACKAGE and LOCALEDIR gettext_package = '-DGETTEXT_PACKAGE="@0@"'.format(meson.project_name()) localedir = '-DLOCALEDIR="@0@"'.format(get_option('prefix') / get_option('localedir')) add_project_arguments(gettext_package, localedir, language: 'cpp') i18n.gettext(meson.project_name(), args: [ '--directory=' + meson.current_source_dir(), '--keyword=S:1' ] ) ================================================ FILE: howdy/src/pam-config/howdy.in ================================================ Name: Howdy Default: yes Priority: 512 Auth-Type: Primary Auth: [success=end default=ignore] @pamdir@/pam_howdy.so ================================================ FILE: howdy/src/paths.py.in ================================================ from pathlib import PurePath # Define the absolute path to the config directory config_dir = PurePath("@config_dir@") # Define the absolute path to the DLib models data directory dlib_data_dir = PurePath("@dlib_data_dir@") # Define the absolute path to the Howdy user models directory user_models_dir = PurePath("@user_models_dir@") # Define path to any howdy logs log_path = PurePath("@log_path@") # Define the absolute path to the Howdy data directory data_dir = PurePath("@data_dir@") ================================================ FILE: howdy/src/paths_factory.py ================================================ from pathlib import PurePath import paths models = [ "shape_predictor_5_face_landmarks.dat", "mmod_human_face_detector.dat", "dlib_face_recognition_resnet_model_v1.dat", ] def dlib_data_dir_path() -> str: return str(paths.dlib_data_dir) def shape_predictor_5_face_landmarks_path() -> str: return str(paths.dlib_data_dir / models[0]) def mmod_human_face_detector_path() -> str: return str(paths.dlib_data_dir / models[1]) def dlib_face_recognition_resnet_model_v1_path() -> str: return str(paths.dlib_data_dir / models[2]) def user_model_path(user: str) -> str: return str(paths.user_models_dir / f"{user}.dat") def config_file_path() -> str: return str(paths.config_dir / "config.ini") def snapshots_dir_path() -> PurePath: return paths.log_path / "snapshots" def snapshot_path(snapshot: str) -> str: return str(snapshots_dir_path() / snapshot) def user_models_dir_path() -> PurePath: return paths.user_models_dir def logo_path() -> str: return str(paths.data_dir / "logo.png") ================================================ FILE: howdy/src/recorders/__init__.py ================================================ ================================================ FILE: howdy/src/recorders/ffmpeg_reader.py ================================================ # Class that simulates the functionality of opencv so howdy can use ffmpeg seamlessly # Import required modules import numpy import sys import re from subprocess import Popen, PIPE from cv2 import CAP_PROP_FRAME_WIDTH from cv2 import CAP_PROP_FRAME_HEIGHT from i18n import _ try: import ffmpeg except ImportError: print(_("Missing ffmpeg module, please run:")) print(" pip3 install ffmpeg-python\n") sys.exit(12) class ffmpeg_reader: """ This class was created to look as similar to the openCV features used in Howdy as possible for overall code cleanliness. """ def __init__(self, device_path, device_format, numframes=10): self.device_path = device_path self.device_format = device_format self.numframes = numframes self.video = () self.num_frames_read = 0 self.height = 0 self.width = 0 self.init_camera = True def set(self, prop, setting): """ Setter method for height and width """ if prop == CAP_PROP_FRAME_WIDTH: self.width = setting elif prop == CAP_PROP_FRAME_HEIGHT: self.height = setting def get(self, prop): """ Getter method for height and width """ if prop == CAP_PROP_FRAME_WIDTH: return self.width elif prop == CAP_PROP_FRAME_HEIGHT: return self.height def probe(self): """ Probe the video device to get height and width info """ # Running this command on ffmpeg unfortunately returns with an exit code of 1, which is silly. # Returns an error code of 1 and this text: "/dev/video2: Immediate exit requested" args = ["ffmpeg", "-f", self.device_format, "-list_formats", "all", "-i", self.device_path] process = Popen(args, stdout=PIPE, stderr=PIPE) out, err = process.communicate() return_code = process.poll() # Worst case scenario, err will equal en empty byte string, b'', so probe will get set to [] here. regex = re.compile(r"\s\d{3,4}x\d{3,4}") probe = regex.findall(str(err.decode("utf-8"))) if not return_code == 1 or len(probe) < 1: # Could not determine the resolution from ffmpeg call. Reverting to ffmpeg.probe() probe = ffmpeg.probe(self.device_path) height = probe["streams"][0]["height"] width = probe["streams"][0]["width"] else: (height, width) = [x.strip() for x in probe[0].split("x")] # Set height and width from probe if they haven't been set already if height.isdigit() and self.get(CAP_PROP_FRAME_HEIGHT) == 0: self.set(CAP_PROP_FRAME_HEIGHT, int(height)) if width.isdigit() and self.get(CAP_PROP_FRAME_WIDTH) == 0: self.set(CAP_PROP_FRAME_WIDTH, int(width)) def record(self, numframes): """ Record a video, saving it to self.video array for processing later """ # Eensure we have set our width and height before we record, otherwise our numpy call will fail if self.get(CAP_PROP_FRAME_WIDTH) == 0 or self.get(CAP_PROP_FRAME_HEIGHT) == 0: self.probe() # Ensure num_frames_read is reset to 0 self.num_frames_read = 0 # Record a predetermined amount of frames from the camera stream, ret = ( ffmpeg .input(self.device_path, format=self.device_format) .output("pipe:", format="rawvideo", pix_fmt="rgb24", vframes=numframes) .run(capture_stdout=True, quiet=True) ) self.video = ( numpy .frombuffer(stream, numpy.uint8) .reshape([-1, self.width, self.height, 3]) ) def read(self): """ Read a single frame from the self.video array. Will record a video if array is empty. """ # First time we are called, we want to initialize the camera by probing it, to ensure we have height/width # and then take numframes of video to fill the buffer for faster recognition. if self.init_camera: self.init_camera = False self.video = () self.record(self.numframes) return 0, self.video # If we are called and self.video is empty, we should record self.numframes to fill the video buffer if self.video == (): self.record(self.numframes) # If we've read max frames, but still are being requested to read more, we simply record another batch. # Note, the video array is 0 based, so if numframes is 10, we must subtract 1 or run into an array index # error. if self.num_frames_read >= (self.numframes - 1): self.record(self.numframes) # Add one to num_frames_read. If we were at 0, that's fine as frame 0 is almost 100% going to be black # as the IR lights aren't fully active yet anyways. Saves us one iteration in the while loop ni add/compare.py. self.num_frames_read += 1 # Return a single frame of video return 0, self.video[self.num_frames_read] def release(self): """ Empty our array. If we had a hold on the camera, we would give it back here. """ self.video = () self.num_frames_read = 0 def grab(self): """ Redirect grab() to read() for compatibility """ self.read() ================================================ FILE: howdy/src/recorders/pyv4l2_reader.py ================================================ # Class that simulates the functionality of opencv so howdy can use v4l2 devices seamlessly # Import required modules. lib4l-dev package is also required. import fcntl import numpy import sys from recorders import v4l2 from cv2 import cvtColor, COLOR_GRAY2BGR, CAP_PROP_FRAME_WIDTH, CAP_PROP_FRAME_HEIGHT from i18n import _ try: from pyv4l2.frame import Frame except ImportError: print(_("Missing pyv4l2 module, please run:")) print(" pip3 install pyv4l2\n") sys.exit(13) class pyv4l2_reader: """ This class was created to look as similar to the openCV features used in Howdy as possible for overall code cleanliness. """ # Init def __init__(self, device_name, device_format): self.device_name = device_name self.device_format = device_format self.height = 0 self.width = 0 self.probe() self.frame = "" def set(self, prop, setting): """ Setter method for height and width """ if prop == CAP_PROP_FRAME_WIDTH: self.width = setting elif prop == CAP_PROP_FRAME_HEIGHT: self.height = setting def get(self, prop): """ Getter method for height and width """ if prop == CAP_PROP_FRAME_WIDTH: return self.width elif prop == CAP_PROP_FRAME_HEIGHT: return self.height def probe(self): """ Probe the video device to get height and width info """ vd = open(self.device_name, 'r') fmt = v4l2.v4l2_format() fmt.type = v4l2.V4L2_BUF_TYPE_VIDEO_CAPTURE ret = fcntl.ioctl(vd, v4l2.VIDIOC_G_FMT, fmt) vd.close() if ret == 0: height = fmt.fmt.pix.height width = fmt.fmt.pix.width else: # Could not determine the resolution from ioctl call. Reverting to slower ffmpeg.probe() method import ffmpeg probe = ffmpeg.probe(self.device_name) height = int(probe['streams'][0]['height']) width = int(probe['streams'][0]['width']) if self.get(CAP_PROP_FRAME_HEIGHT) == 0: self.set(CAP_PROP_FRAME_HEIGHT, int(height)) if self.get(CAP_PROP_FRAME_WIDTH) == 0: self.set(CAP_PROP_FRAME_WIDTH, int(width)) def record(self): """ Start recording """ self.frame = Frame(self.device_name) def grab(self): """ Read a single frame from the IR camera. """ self.read() def read(self): """ Read a single frame from the IR camera. """ if not self.frame: self.record() # Grab a raw frame from the camera frame_data = self.frame.get_frame() # Convert the raw frame_date to a numpy array img = (numpy.frombuffer(frame_data, numpy.uint8)) # Convert the numpy array to a proper grayscale image array img_bgr = cvtColor(img, COLOR_GRAY2BGR) # Convert the grayscale image array into a proper RGB style numpy array img2 = (numpy.frombuffer(img_bgr, numpy.uint8).reshape([352, 352, 3])) # Return a single frame of video return 0, img2 def release(self): """ Empty our array. If we had a hold on the camera, we would give it back here. """ self.video = () self.num_frames_read = 0 if self.frame: self.frame.close() ================================================ FILE: howdy/src/recorders/v4l2.py ================================================ # Python bindings for the v4l2 userspace api # Copyright (C) 1999-2009 the contributors # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # Alternatively you can redistribute this file under the terms of the # BSD license as stated below: # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in # the documentation and/or other materials provided with the # distribution. # 3. The names of its contributors may not be used to endorse or promote # products derived from this software without specific prior written # permission. # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ Python bindings for the v4l2 userspace api in Linux 2.6.34 """ # see linux/videodev2.h import ctypes _IOC_NRBITS = 8 _IOC_TYPEBITS = 8 _IOC_SIZEBITS = 14 _IOC_DIRBITS = 2 _IOC_NRSHIFT = 0 _IOC_TYPESHIFT = _IOC_NRSHIFT + _IOC_NRBITS _IOC_SIZESHIFT = _IOC_TYPESHIFT + _IOC_TYPEBITS _IOC_DIRSHIFT = _IOC_SIZESHIFT + _IOC_SIZEBITS _IOC_NONE = 0 _IOC_WRITE = 1 _IOC_READ = 2 def _IOC(dir_, type_, nr, size): return ( ctypes.c_int32(dir_ << _IOC_DIRSHIFT).value | ctypes.c_int32(ord(type_) << _IOC_TYPESHIFT).value | ctypes.c_int32(nr << _IOC_NRSHIFT).value | ctypes.c_int32(size << _IOC_SIZESHIFT).value) def _IOC_TYPECHECK(t): return ctypes.sizeof(t) def _IO(type_, nr): return _IOC(_IOC_NONE, type_, nr, 0) def _IOW(type_, nr, size): return _IOC(_IOC_WRITE, type_, nr, _IOC_TYPECHECK(size)) def _IOR(type_, nr, size): return _IOC(_IOC_READ, type_, nr, _IOC_TYPECHECK(size)) def _IOWR(type_, nr, size): return _IOC(_IOC_READ | _IOC_WRITE, type_, nr, _IOC_TYPECHECK(size)) # # type alias # enum = ctypes.c_uint c_int = ctypes.c_int # # time # class timeval(ctypes.Structure): _fields_ = [ ('secs', ctypes.c_long), ('usecs', ctypes.c_long), ] # # v4l2 # VIDEO_MAX_FRAME = 32 VID_TYPE_CAPTURE = 1 VID_TYPE_TUNER = 2 VID_TYPE_TELETEXT = 4 VID_TYPE_OVERLAY = 8 VID_TYPE_CHROMAKEY = 16 VID_TYPE_CLIPPING = 32 VID_TYPE_FRAMERAM = 64 VID_TYPE_SCALES = 128 VID_TYPE_MONOCHROME = 256 VID_TYPE_SUBCAPTURE = 512 VID_TYPE_MPEG_DECODER = 1024 VID_TYPE_MPEG_ENCODER = 2048 VID_TYPE_MJPEG_DECODER = 4096 VID_TYPE_MJPEG_ENCODER = 8192 def v4l2_fourcc(a, b, c, d): return ord(a) | (ord(b) << 8) | (ord(c) << 16) | (ord(d) << 24) v4l2_field = enum ( V4L2_FIELD_ANY, V4L2_FIELD_NONE, V4L2_FIELD_TOP, V4L2_FIELD_BOTTOM, V4L2_FIELD_INTERLACED, V4L2_FIELD_SEQ_TB, V4L2_FIELD_SEQ_BT, V4L2_FIELD_ALTERNATE, V4L2_FIELD_INTERLACED_TB, V4L2_FIELD_INTERLACED_BT, ) = range(10) def V4L2_FIELD_HAS_TOP(field): return ( field == V4L2_FIELD_TOP or field == V4L2_FIELD_INTERLACED or field == V4L2_FIELD_INTERLACED_TB or field == V4L2_FIELD_INTERLACED_BT or field == V4L2_FIELD_SEQ_TB or field == V4L2_FIELD_SEQ_BT) def V4L2_FIELD_HAS_BOTTOM(field): return ( field == V4L2_FIELD_BOTTOM or field == V4L2_FIELD_INTERLACED or field == V4L2_FIELD_INTERLACED_TB or field == V4L2_FIELD_INTERLACED_BT or field == V4L2_FIELD_SEQ_TB or field == V4L2_FIELD_SEQ_BT) def V4L2_FIELD_HAS_BOTH(field): return ( field == V4L2_FIELD_INTERLACED or field == V4L2_FIELD_INTERLACED_TB or field == V4L2_FIELD_INTERLACED_BT or field == V4L2_FIELD_SEQ_TB or field == V4L2_FIELD_SEQ_BT) v4l2_buf_type = enum ( V4L2_BUF_TYPE_VIDEO_CAPTURE, V4L2_BUF_TYPE_VIDEO_OUTPUT, V4L2_BUF_TYPE_VIDEO_OVERLAY, V4L2_BUF_TYPE_VBI_CAPTURE, V4L2_BUF_TYPE_VBI_OUTPUT, V4L2_BUF_TYPE_SLICED_VBI_CAPTURE, V4L2_BUF_TYPE_SLICED_VBI_OUTPUT, V4L2_BUF_TYPE_VIDEO_OUTPUT_OVERLAY, V4L2_BUF_TYPE_PRIVATE, ) = list(range(1, 9)) + [0x80] v4l2_ctrl_type = enum ( V4L2_CTRL_TYPE_INTEGER, V4L2_CTRL_TYPE_BOOLEAN, V4L2_CTRL_TYPE_MENU, V4L2_CTRL_TYPE_BUTTON, V4L2_CTRL_TYPE_INTEGER64, V4L2_CTRL_TYPE_CTRL_CLASS, V4L2_CTRL_TYPE_STRING, ) = range(1, 8) v4l2_tuner_type = enum ( V4L2_TUNER_RADIO, V4L2_TUNER_ANALOG_TV, V4L2_TUNER_DIGITAL_TV, ) = range(1, 4) v4l2_memory = enum ( V4L2_MEMORY_MMAP, V4L2_MEMORY_USERPTR, V4L2_MEMORY_OVERLAY, ) = range(1, 4) v4l2_colorspace = enum ( V4L2_COLORSPACE_SMPTE170M, V4L2_COLORSPACE_SMPTE240M, V4L2_COLORSPACE_REC709, V4L2_COLORSPACE_BT878, V4L2_COLORSPACE_470_SYSTEM_M, V4L2_COLORSPACE_470_SYSTEM_BG, V4L2_COLORSPACE_JPEG, V4L2_COLORSPACE_SRGB, ) = range(1, 9) v4l2_priority = enum ( V4L2_PRIORITY_UNSET, V4L2_PRIORITY_BACKGROUND, V4L2_PRIORITY_INTERACTIVE, V4L2_PRIORITY_RECORD, V4L2_PRIORITY_DEFAULT, ) = list(range(0, 4)) + [2] class v4l2_rect(ctypes.Structure): _fields_ = [ ('left', ctypes.c_int32), ('top', ctypes.c_int32), ('width', ctypes.c_int32), ('height', ctypes.c_int32), ] class v4l2_fract(ctypes.Structure): _fields_ = [ ('numerator', ctypes.c_uint32), ('denominator', ctypes.c_uint32), ] # # Driver capabilities # class v4l2_capability(ctypes.Structure): _fields_ = [ ('driver', ctypes.c_char * 16), ('card', ctypes.c_char * 32), ('bus_info', ctypes.c_char * 32), ('version', ctypes.c_uint32), ('capabilities', ctypes.c_uint32), ('reserved', ctypes.c_uint32 * 4), ] # # Values for 'capabilities' field # V4L2_CAP_VIDEO_CAPTURE = 0x00000001 V4L2_CAP_VIDEO_OUTPUT = 0x00000002 V4L2_CAP_VIDEO_OVERLAY = 0x00000004 V4L2_CAP_VBI_CAPTURE = 0x00000010 V4L2_CAP_VBI_OUTPUT = 0x00000020 V4L2_CAP_SLICED_VBI_CAPTURE = 0x00000040 V4L2_CAP_SLICED_VBI_OUTPUT = 0x00000080 V4L2_CAP_RDS_CAPTURE = 0x00000100 V4L2_CAP_VIDEO_OUTPUT_OVERLAY = 0x00000200 V4L2_CAP_HW_FREQ_SEEK = 0x00000400 V4L2_CAP_RDS_OUTPUT = 0x00000800 V4L2_CAP_TUNER = 0x00010000 V4L2_CAP_AUDIO = 0x00020000 V4L2_CAP_RADIO = 0x00040000 V4L2_CAP_MODULATOR = 0x00080000 V4L2_CAP_READWRITE = 0x01000000 V4L2_CAP_ASYNCIO = 0x02000000 V4L2_CAP_STREAMING = 0x04000000 # # Video image format # class v4l2_pix_format(ctypes.Structure): _fields_ = [ ('width', ctypes.c_uint32), ('height', ctypes.c_uint32), ('pixelformat', ctypes.c_uint32), ('field', v4l2_field), ('bytesperline', ctypes.c_uint32), ('sizeimage', ctypes.c_uint32), ('colorspace', v4l2_colorspace), ('priv', ctypes.c_uint32), ] # RGB formats V4L2_PIX_FMT_RGB332 = v4l2_fourcc('R', 'G', 'B', '1') V4L2_PIX_FMT_RGB444 = v4l2_fourcc('R', '4', '4', '4') V4L2_PIX_FMT_RGB555 = v4l2_fourcc('R', 'G', 'B', 'O') V4L2_PIX_FMT_RGB565 = v4l2_fourcc('R', 'G', 'B', 'P') V4L2_PIX_FMT_RGB555X = v4l2_fourcc('R', 'G', 'B', 'Q') V4L2_PIX_FMT_RGB565X = v4l2_fourcc('R', 'G', 'B', 'R') V4L2_PIX_FMT_BGR24 = v4l2_fourcc('B', 'G', 'R', '3') V4L2_PIX_FMT_RGB24 = v4l2_fourcc('R', 'G', 'B', '3') V4L2_PIX_FMT_BGR32 = v4l2_fourcc('B', 'G', 'R', '4') V4L2_PIX_FMT_RGB32 = v4l2_fourcc('R', 'G', 'B', '4') # Grey formats V4L2_PIX_FMT_GREY = v4l2_fourcc('G', 'R', 'E', 'Y') V4L2_PIX_FMT_Y10 = v4l2_fourcc('Y', '1', '0', ' ') V4L2_PIX_FMT_Y16 = v4l2_fourcc('Y', '1', '6', ' ') # Palette formats V4L2_PIX_FMT_PAL8 = v4l2_fourcc('P', 'A', 'L', '8') # Luminance+Chrominance formats V4L2_PIX_FMT_YVU410 = v4l2_fourcc('Y', 'V', 'U', '9') V4L2_PIX_FMT_YVU420 = v4l2_fourcc('Y', 'V', '1', '2') V4L2_PIX_FMT_YUYV = v4l2_fourcc('Y', 'U', 'Y', 'V') V4L2_PIX_FMT_YYUV = v4l2_fourcc('Y', 'Y', 'U', 'V') V4L2_PIX_FMT_YVYU = v4l2_fourcc('Y', 'V', 'Y', 'U') V4L2_PIX_FMT_UYVY = v4l2_fourcc('U', 'Y', 'V', 'Y') V4L2_PIX_FMT_VYUY = v4l2_fourcc('V', 'Y', 'U', 'Y') V4L2_PIX_FMT_YUV422P = v4l2_fourcc('4', '2', '2', 'P') V4L2_PIX_FMT_YUV411P = v4l2_fourcc('4', '1', '1', 'P') V4L2_PIX_FMT_Y41P = v4l2_fourcc('Y', '4', '1', 'P') V4L2_PIX_FMT_YUV444 = v4l2_fourcc('Y', '4', '4', '4') V4L2_PIX_FMT_YUV555 = v4l2_fourcc('Y', 'U', 'V', 'O') V4L2_PIX_FMT_YUV565 = v4l2_fourcc('Y', 'U', 'V', 'P') V4L2_PIX_FMT_YUV32 = v4l2_fourcc('Y', 'U', 'V', '4') V4L2_PIX_FMT_YUV410 = v4l2_fourcc('Y', 'U', 'V', '9') V4L2_PIX_FMT_YUV420 = v4l2_fourcc('Y', 'U', '1', '2') V4L2_PIX_FMT_HI240 = v4l2_fourcc('H', 'I', '2', '4') V4L2_PIX_FMT_HM12 = v4l2_fourcc('H', 'M', '1', '2') # two planes -- one Y, one Cr + Cb interleaved V4L2_PIX_FMT_NV12 = v4l2_fourcc('N', 'V', '1', '2') V4L2_PIX_FMT_NV21 = v4l2_fourcc('N', 'V', '2', '1') V4L2_PIX_FMT_NV16 = v4l2_fourcc('N', 'V', '1', '6') V4L2_PIX_FMT_NV61 = v4l2_fourcc('N', 'V', '6', '1') # Bayer formats - see http://www.siliconimaging.com/RGB%20Bayer.htm V4L2_PIX_FMT_SBGGR8 = v4l2_fourcc('B', 'A', '8', '1') V4L2_PIX_FMT_SGBRG8 = v4l2_fourcc('G', 'B', 'R', 'G') V4L2_PIX_FMT_SGRBG8 = v4l2_fourcc('G', 'R', 'B', 'G') V4L2_PIX_FMT_SRGGB8 = v4l2_fourcc('R', 'G', 'G', 'B') V4L2_PIX_FMT_SBGGR10 = v4l2_fourcc('B', 'G', '1', '0') V4L2_PIX_FMT_SGBRG10 = v4l2_fourcc('G', 'B', '1', '0') V4L2_PIX_FMT_SGRBG10 = v4l2_fourcc('B', 'A', '1', '0') V4L2_PIX_FMT_SRGGB10 = v4l2_fourcc('R', 'G', '1', '0') V4L2_PIX_FMT_SGRBG10DPCM8 = v4l2_fourcc('B', 'D', '1', '0') V4L2_PIX_FMT_SBGGR16 = v4l2_fourcc('B', 'Y', 'R', '2') # compressed formats V4L2_PIX_FMT_MJPEG = v4l2_fourcc('M', 'J', 'P', 'G') V4L2_PIX_FMT_JPEG = v4l2_fourcc('J', 'P', 'E', 'G') V4L2_PIX_FMT_DV = v4l2_fourcc('d', 'v', 's', 'd') V4L2_PIX_FMT_MPEG = v4l2_fourcc('M', 'P', 'E', 'G') # Vendor-specific formats V4L2_PIX_FMT_CPIA1 = v4l2_fourcc('C', 'P', 'I', 'A') V4L2_PIX_FMT_WNVA = v4l2_fourcc('W', 'N', 'V', 'A') V4L2_PIX_FMT_SN9C10X = v4l2_fourcc('S', '9', '1', '0') V4L2_PIX_FMT_SN9C20X_I420 = v4l2_fourcc('S', '9', '2', '0') V4L2_PIX_FMT_PWC1 = v4l2_fourcc('P', 'W', 'C', '1') V4L2_PIX_FMT_PWC2 = v4l2_fourcc('P', 'W', 'C', '2') V4L2_PIX_FMT_ET61X251 = v4l2_fourcc('E', '6', '2', '5') V4L2_PIX_FMT_SPCA501 = v4l2_fourcc('S', '5', '0', '1') V4L2_PIX_FMT_SPCA505 = v4l2_fourcc('S', '5', '0', '5') V4L2_PIX_FMT_SPCA508 = v4l2_fourcc('S', '5', '0', '8') V4L2_PIX_FMT_SPCA561 = v4l2_fourcc('S', '5', '6', '1') V4L2_PIX_FMT_PAC207 = v4l2_fourcc('P', '2', '0', '7') V4L2_PIX_FMT_MR97310A = v4l2_fourcc('M', '3', '1', '0') V4L2_PIX_FMT_SN9C2028 = v4l2_fourcc('S', 'O', 'N', 'X') V4L2_PIX_FMT_SQ905C = v4l2_fourcc('9', '0', '5', 'C') V4L2_PIX_FMT_PJPG = v4l2_fourcc('P', 'J', 'P', 'G') V4L2_PIX_FMT_OV511 = v4l2_fourcc('O', '5', '1', '1') V4L2_PIX_FMT_OV518 = v4l2_fourcc('O', '5', '1', '8') V4L2_PIX_FMT_STV0680 = v4l2_fourcc('S', '6', '8', '0') # # Format enumeration # class v4l2_fmtdesc(ctypes.Structure): _fields_ = [ ('index', ctypes.c_uint32), ('type', ctypes.c_int), ('flags', ctypes.c_uint32), ('description', ctypes.c_char * 32), ('pixelformat', ctypes.c_uint32), ('reserved', ctypes.c_uint32 * 4), ] V4L2_FMT_FLAG_COMPRESSED = 0x0001 V4L2_FMT_FLAG_EMULATED = 0x0002 # # Experimental frame size and frame rate enumeration # v4l2_frmsizetypes = enum ( V4L2_FRMSIZE_TYPE_DISCRETE, V4L2_FRMSIZE_TYPE_CONTINUOUS, V4L2_FRMSIZE_TYPE_STEPWISE, ) = range(1, 4) class v4l2_frmsize_discrete(ctypes.Structure): _fields_ = [ ('width', ctypes.c_uint32), ('height', ctypes.c_uint32), ] class v4l2_frmsize_stepwise(ctypes.Structure): _fields_ = [ ('min_width', ctypes.c_uint32), ('min_height', ctypes.c_uint32), ('step_width', ctypes.c_uint32), ('min_height', ctypes.c_uint32), ('max_height', ctypes.c_uint32), ('step_height', ctypes.c_uint32), ] class v4l2_frmsizeenum(ctypes.Structure): class _u(ctypes.Union): _fields_ = [ ('discrete', v4l2_frmsize_discrete), ('stepwise', v4l2_frmsize_stepwise), ] _fields_ = [ ('index', ctypes.c_uint32), ('pixel_format', ctypes.c_uint32), ('type', ctypes.c_uint32), ('_u', _u), ('reserved', ctypes.c_uint32 * 2) ] _anonymous_ = ('_u',) # # Frame rate enumeration # v4l2_frmivaltypes = enum ( V4L2_FRMIVAL_TYPE_DISCRETE, V4L2_FRMIVAL_TYPE_CONTINUOUS, V4L2_FRMIVAL_TYPE_STEPWISE, ) = range(1, 4) class v4l2_frmival_stepwise(ctypes.Structure): _fields_ = [ ('min', v4l2_fract), ('max', v4l2_fract), ('step', v4l2_fract), ] class v4l2_frmivalenum(ctypes.Structure): class _u(ctypes.Union): _fields_ = [ ('discrete', v4l2_fract), ('stepwise', v4l2_frmival_stepwise), ] _fields_ = [ ('index', ctypes.c_uint32), ('pixel_format', ctypes.c_uint32), ('width', ctypes.c_uint32), ('height', ctypes.c_uint32), ('type', ctypes.c_uint32), ('_u', _u), ('reserved', ctypes.c_uint32 * 2), ] _anonymous_ = ('_u',) # # Timecode # class v4l2_timecode(ctypes.Structure): _fields_ = [ ('type', ctypes.c_uint32), ('flags', ctypes.c_uint32), ('frames', ctypes.c_uint8), ('seconds', ctypes.c_uint8), ('minutes', ctypes.c_uint8), ('hours', ctypes.c_uint8), ('userbits', ctypes.c_uint8 * 4), ] V4L2_TC_TYPE_24FPS = 1 V4L2_TC_TYPE_25FPS = 2 V4L2_TC_TYPE_30FPS = 3 V4L2_TC_TYPE_50FPS = 4 V4L2_TC_TYPE_60FPS = 5 V4L2_TC_FLAG_DROPFRAME = 0x0001 V4L2_TC_FLAG_COLORFRAME = 0x0002 V4L2_TC_USERBITS_field = 0x000C V4L2_TC_USERBITS_USERDEFINED = 0x0000 V4L2_TC_USERBITS_8BITCHARS = 0x0008 class v4l2_jpegcompression(ctypes.Structure): _fields_ = [ ('quality', ctypes.c_int), ('APPn', ctypes.c_int), ('APP_len', ctypes.c_int), ('APP_data', ctypes.c_char * 60), ('COM_len', ctypes.c_int), ('COM_data', ctypes.c_char * 60), ('jpeg_markers', ctypes.c_uint32), ] V4L2_JPEG_MARKER_DHT = 1 << 3 V4L2_JPEG_MARKER_DQT = 1 << 4 V4L2_JPEG_MARKER_DRI = 1 << 5 V4L2_JPEG_MARKER_COM = 1 << 6 V4L2_JPEG_MARKER_APP = 1 << 7 # # Memory-mapping buffers # class v4l2_requestbuffers(ctypes.Structure): _fields_ = [ ('count', ctypes.c_uint32), ('type', v4l2_buf_type), ('memory', v4l2_memory), ('reserved', ctypes.c_uint32 * 2), ] class v4l2_buffer(ctypes.Structure): class _u(ctypes.Union): _fields_ = [ ('offset', ctypes.c_uint32), ('userptr', ctypes.c_ulong), ] _fields_ = [ ('index', ctypes.c_uint32), ('type', v4l2_buf_type), ('bytesused', ctypes.c_uint32), ('flags', ctypes.c_uint32), ('field', v4l2_field), ('timestamp', timeval), ('timecode', v4l2_timecode), ('sequence', ctypes.c_uint32), ('memory', v4l2_memory), ('m', _u), ('length', ctypes.c_uint32), ('input', ctypes.c_uint32), ('reserved', ctypes.c_uint32), ] V4L2_BUF_FLAG_MAPPED = 0x0001 V4L2_BUF_FLAG_QUEUED = 0x0002 V4L2_BUF_FLAG_DONE = 0x0004 V4L2_BUF_FLAG_KEYFRAME = 0x0008 V4L2_BUF_FLAG_PFRAME = 0x0010 V4L2_BUF_FLAG_BFRAME = 0x0020 V4L2_BUF_FLAG_TIMECODE = 0x0100 V4L2_BUF_FLAG_INPUT = 0x0200 # # Overlay preview # class v4l2_framebuffer(ctypes.Structure): _fields_ = [ ('capability', ctypes.c_uint32), ('flags', ctypes.c_uint32), ('base', ctypes.c_void_p), ('fmt', v4l2_pix_format), ] V4L2_FBUF_CAP_EXTERNOVERLAY = 0x0001 V4L2_FBUF_CAP_CHROMAKEY = 0x0002 V4L2_FBUF_CAP_LIST_CLIPPING = 0x0004 V4L2_FBUF_CAP_BITMAP_CLIPPING = 0x0008 V4L2_FBUF_CAP_LOCAL_ALPHA = 0x0010 V4L2_FBUF_CAP_GLOBAL_ALPHA = 0x0020 V4L2_FBUF_CAP_LOCAL_INV_ALPHA = 0x0040 V4L2_FBUF_CAP_SRC_CHROMAKEY = 0x0080 V4L2_FBUF_FLAG_PRIMARY = 0x0001 V4L2_FBUF_FLAG_OVERLAY = 0x0002 V4L2_FBUF_FLAG_CHROMAKEY = 0x0004 V4L2_FBUF_FLAG_LOCAL_ALPHA = 0x0008 V4L2_FBUF_FLAG_GLOBAL_ALPHA = 0x0010 V4L2_FBUF_FLAG_LOCAL_INV_ALPHA = 0x0020 V4L2_FBUF_FLAG_SRC_CHROMAKEY = 0x0040 class v4l2_clip(ctypes.Structure): pass v4l2_clip._fields_ = [ ('c', v4l2_rect), ('next', ctypes.POINTER(v4l2_clip)), ] class v4l2_window(ctypes.Structure): _fields_ = [ ('w', v4l2_rect), ('field', v4l2_field), ('chromakey', ctypes.c_uint32), ('clips', ctypes.POINTER(v4l2_clip)), ('clipcount', ctypes.c_uint32), ('bitmap', ctypes.c_void_p), ('global_alpha', ctypes.c_uint8), ] # # Capture parameters # class v4l2_captureparm(ctypes.Structure): _fields_ = [ ('capability', ctypes.c_uint32), ('capturemode', ctypes.c_uint32), ('timeperframe', v4l2_fract), ('extendedmode', ctypes.c_uint32), ('readbuffers', ctypes.c_uint32), ('reserved', ctypes.c_uint32 * 4), ] V4L2_MODE_HIGHQUALITY = 0x0001 V4L2_CAP_TIMEPERFRAME = 0x1000 class v4l2_outputparm(ctypes.Structure): _fields_ = [ ('capability', ctypes.c_uint32), ('outputmode', ctypes.c_uint32), ('timeperframe', v4l2_fract), ('extendedmode', ctypes.c_uint32), ('writebuffers', ctypes.c_uint32), ('reserved', ctypes.c_uint32 * 4), ] # # Input image cropping # class v4l2_cropcap(ctypes.Structure): _fields_ = [ ('type', v4l2_buf_type), ('bounds', v4l2_rect), ('defrect', v4l2_rect), ('pixelaspect', v4l2_fract), ] class v4l2_crop(ctypes.Structure): _fields_ = [ ('type', ctypes.c_int), ('c', v4l2_rect), ] # # Analog video standard # v4l2_std_id = ctypes.c_uint64 V4L2_STD_PAL_B = 0x00000001 V4L2_STD_PAL_B1 = 0x00000002 V4L2_STD_PAL_G = 0x00000004 V4L2_STD_PAL_H = 0x00000008 V4L2_STD_PAL_I = 0x00000010 V4L2_STD_PAL_D = 0x00000020 V4L2_STD_PAL_D1 = 0x00000040 V4L2_STD_PAL_K = 0x00000080 V4L2_STD_PAL_M = 0x00000100 V4L2_STD_PAL_N = 0x00000200 V4L2_STD_PAL_Nc = 0x00000400 V4L2_STD_PAL_60 = 0x00000800 V4L2_STD_NTSC_M = 0x00001000 V4L2_STD_NTSC_M_JP = 0x00002000 V4L2_STD_NTSC_443 = 0x00004000 V4L2_STD_NTSC_M_KR = 0x00008000 V4L2_STD_SECAM_B = 0x00010000 V4L2_STD_SECAM_D = 0x00020000 V4L2_STD_SECAM_G = 0x00040000 V4L2_STD_SECAM_H = 0x00080000 V4L2_STD_SECAM_K = 0x00100000 V4L2_STD_SECAM_K1 = 0x00200000 V4L2_STD_SECAM_L = 0x00400000 V4L2_STD_SECAM_LC = 0x00800000 V4L2_STD_ATSC_8_VSB = 0x01000000 V4L2_STD_ATSC_16_VSB = 0x02000000 # some common needed stuff V4L2_STD_PAL_BG = (V4L2_STD_PAL_B | V4L2_STD_PAL_B1 | V4L2_STD_PAL_G) V4L2_STD_PAL_DK = (V4L2_STD_PAL_D | V4L2_STD_PAL_D1 | V4L2_STD_PAL_K) V4L2_STD_PAL = (V4L2_STD_PAL_BG | V4L2_STD_PAL_DK | V4L2_STD_PAL_H | V4L2_STD_PAL_I) V4L2_STD_NTSC = (V4L2_STD_NTSC_M | V4L2_STD_NTSC_M_JP | V4L2_STD_NTSC_M_KR) V4L2_STD_SECAM_DK = (V4L2_STD_SECAM_D | V4L2_STD_SECAM_K | V4L2_STD_SECAM_K1) V4L2_STD_SECAM = (V4L2_STD_SECAM_B | V4L2_STD_SECAM_G | V4L2_STD_SECAM_H | V4L2_STD_SECAM_DK | V4L2_STD_SECAM_L | V4L2_STD_SECAM_LC) V4L2_STD_525_60 = (V4L2_STD_PAL_M | V4L2_STD_PAL_60 | V4L2_STD_NTSC | V4L2_STD_NTSC_443) V4L2_STD_625_50 = (V4L2_STD_PAL | V4L2_STD_PAL_N | V4L2_STD_PAL_Nc | V4L2_STD_SECAM) V4L2_STD_ATSC = (V4L2_STD_ATSC_8_VSB | V4L2_STD_ATSC_16_VSB) V4L2_STD_UNKNOWN = 0 V4L2_STD_ALL = (V4L2_STD_525_60 | V4L2_STD_625_50) # some merged standards V4L2_STD_MN = (V4L2_STD_PAL_M | V4L2_STD_PAL_N | V4L2_STD_PAL_Nc | V4L2_STD_NTSC) V4L2_STD_B = (V4L2_STD_PAL_B | V4L2_STD_PAL_B1 | V4L2_STD_SECAM_B) V4L2_STD_GH = (V4L2_STD_PAL_G | V4L2_STD_PAL_H|V4L2_STD_SECAM_G | V4L2_STD_SECAM_H) V4L2_STD_DK = (V4L2_STD_PAL_DK | V4L2_STD_SECAM_DK) class v4l2_standard(ctypes.Structure): _fields_ = [ ('index', ctypes.c_uint32), ('id', v4l2_std_id), ('name', ctypes.c_char * 24), ('frameperiod', v4l2_fract), ('framelines', ctypes.c_uint32), ('reserved', ctypes.c_uint32 * 4), ] # # Video timings dv preset # class v4l2_dv_preset(ctypes.Structure): _fields_ = [ ('preset', ctypes.c_uint32), ('reserved', ctypes.c_uint32 * 4) ] # # DV preset enumeration # class v4l2_dv_enum_preset(ctypes.Structure): _fields_ = [ ('index', ctypes.c_uint32), ('preset', ctypes.c_uint32), ('name', ctypes.c_char * 32), ('width', ctypes.c_uint32), ('height', ctypes.c_uint32), ('reserved', ctypes.c_uint32 * 4), ] # # DV preset values # V4L2_DV_INVALID = 0 V4L2_DV_480P59_94 = 1 V4L2_DV_576P50 = 2 V4L2_DV_720P24 = 3 V4L2_DV_720P25 = 4 V4L2_DV_720P30 = 5 V4L2_DV_720P50 = 6 V4L2_DV_720P59_94 = 7 V4L2_DV_720P60 = 8 V4L2_DV_1080I29_97 = 9 V4L2_DV_1080I30 = 10 V4L2_DV_1080I25 = 11 V4L2_DV_1080I50 = 12 V4L2_DV_1080I60 = 13 V4L2_DV_1080P24 = 14 V4L2_DV_1080P25 = 15 V4L2_DV_1080P30 = 16 V4L2_DV_1080P50 = 17 V4L2_DV_1080P60 = 18 # # DV BT timings # class v4l2_bt_timings(ctypes.Structure): _fields_ = [ ('width', ctypes.c_uint32), ('height', ctypes.c_uint32), ('interlaced', ctypes.c_uint32), ('polarities', ctypes.c_uint32), ('pixelclock', ctypes.c_uint64), ('hfrontporch', ctypes.c_uint32), ('hsync', ctypes.c_uint32), ('hbackporch', ctypes.c_uint32), ('vfrontporch', ctypes.c_uint32), ('vsync', ctypes.c_uint32), ('vbackporch', ctypes.c_uint32), ('il_vfrontporch', ctypes.c_uint32), ('il_vsync', ctypes.c_uint32), ('il_vbackporch', ctypes.c_uint32), ('reserved', ctypes.c_uint32 * 16), ] _pack_ = True # Interlaced or progressive format V4L2_DV_PROGRESSIVE = 0 V4L2_DV_INTERLACED = 1 # Polarities. If bit is not set, it is assumed to be negative polarity V4L2_DV_VSYNC_POS_POL = 0x00000001 V4L2_DV_HSYNC_POS_POL = 0x00000002 class v4l2_dv_timings(ctypes.Structure): class _u(ctypes.Union): _fields_ = [ ('bt', v4l2_bt_timings), ('reserved', ctypes.c_uint32 * 32), ] _fields_ = [ ('type', ctypes.c_uint32), ('_u', _u), ] _anonymous_ = ('_u',) _pack_ = True # Values for the type field V4L2_DV_BT_656_1120 = 0 # # Video inputs # class v4l2_input(ctypes.Structure): _fields_ = [ ('index', ctypes.c_uint32), ('name', ctypes.c_char * 32), ('type', ctypes.c_uint32), ('audioset', ctypes.c_uint32), ('tuner', ctypes.c_uint32), ('std', v4l2_std_id), ('status', ctypes.c_uint32), ('reserved', ctypes.c_uint32 * 4), ] V4L2_INPUT_TYPE_TUNER = 1 V4L2_INPUT_TYPE_CAMERA = 2 V4L2_IN_ST_NO_POWER = 0x00000001 V4L2_IN_ST_NO_SIGNAL = 0x00000002 V4L2_IN_ST_NO_COLOR = 0x00000004 V4L2_IN_ST_HFLIP = 0x00000010 V4L2_IN_ST_VFLIP = 0x00000020 V4L2_IN_ST_NO_H_LOCK = 0x00000100 V4L2_IN_ST_COLOR_KILL = 0x00000200 V4L2_IN_ST_NO_SYNC = 0x00010000 V4L2_IN_ST_NO_EQU = 0x00020000 V4L2_IN_ST_NO_CARRIER = 0x00040000 V4L2_IN_ST_MACROVISION = 0x01000000 V4L2_IN_ST_NO_ACCESS = 0x02000000 V4L2_IN_ST_VTR = 0x04000000 V4L2_IN_CAP_PRESETS = 0x00000001 V4L2_IN_CAP_CUSTOM_TIMINGS = 0x00000002 V4L2_IN_CAP_STD = 0x00000004 # # Video outputs # class v4l2_output(ctypes.Structure): _fields_ = [ ('index', ctypes.c_uint32), ('name', ctypes.c_char * 32), ('type', ctypes.c_uint32), ('audioset', ctypes.c_uint32), ('modulator', ctypes.c_uint32), ('std', v4l2_std_id), ('reserved', ctypes.c_uint32 * 4), ] V4L2_OUTPUT_TYPE_MODULATOR = 1 V4L2_OUTPUT_TYPE_ANALOG = 2 V4L2_OUTPUT_TYPE_ANALOGVGAOVERLAY = 3 V4L2_OUT_CAP_PRESETS = 0x00000001 V4L2_OUT_CAP_CUSTOM_TIMINGS = 0x00000002 V4L2_OUT_CAP_STD = 0x00000004 # # Controls # class v4l2_control(ctypes.Structure): _fields_ = [ ('id', ctypes.c_uint32), ('value', ctypes.c_int32), ] class v4l2_ext_control(ctypes.Structure): class _u(ctypes.Union): _fields_ = [ ('value', ctypes.c_int32), ('value64', ctypes.c_int64), ('reserved', ctypes.c_void_p), ] _fields_ = [ ('id', ctypes.c_uint32), ('reserved2', ctypes.c_uint32 * 2), ('_u', _u) ] _anonymous_ = ('_u',) _pack_ = True class v4l2_ext_controls(ctypes.Structure): _fields_ = [ ('ctrl_class', ctypes.c_uint32), ('count', ctypes.c_uint32), ('error_idx', ctypes.c_uint32), ('reserved', ctypes.c_uint32 * 2), ('controls', ctypes.POINTER(v4l2_ext_control)), ] V4L2_CTRL_CLASS_USER = 0x00980000 V4L2_CTRL_CLASS_MPEG = 0x00990000 V4L2_CTRL_CLASS_CAMERA = 0x009a0000 V4L2_CTRL_CLASS_FM_TX = 0x009b0000 def V4L2_CTRL_ID_MASK(): return 0x0fffffff def V4L2_CTRL_ID2CLASS(id_): return id_ & 0x0fff0000 # unsigned long def V4L2_CTRL_DRIVER_PRIV(id_): return (id_ & 0xffff) >= 0x1000 class v4l2_queryctrl(ctypes.Structure): _fields_ = [ ('id', ctypes.c_uint32), ('type', v4l2_ctrl_type), ('name', ctypes.c_char * 32), ('minimum', ctypes.c_int32), ('maximum', ctypes.c_int32), ('step', ctypes.c_int32), ('default', ctypes.c_int32), ('flags', ctypes.c_uint32), ('reserved', ctypes.c_uint32 * 2), ] class v4l2_querymenu(ctypes.Structure): _fields_ = [ ('id', ctypes.c_uint32), ('index', ctypes.c_uint32), ('name', ctypes.c_char * 32), ('reserved', ctypes.c_uint32), ] V4L2_CTRL_FLAG_DISABLED = 0x0001 V4L2_CTRL_FLAG_GRABBED = 0x0002 V4L2_CTRL_FLAG_READ_ONLY = 0x0004 V4L2_CTRL_FLAG_UPDATE = 0x0008 V4L2_CTRL_FLAG_INACTIVE = 0x0010 V4L2_CTRL_FLAG_SLIDER = 0x0020 V4L2_CTRL_FLAG_WRITE_ONLY = 0x0040 V4L2_CTRL_FLAG_NEXT_CTRL = 0x80000000 V4L2_CID_BASE = V4L2_CTRL_CLASS_USER | 0x900 V4L2_CID_USER_BASE = V4L2_CID_BASE V4L2_CID_PRIVATE_BASE = 0x08000000 V4L2_CID_USER_CLASS = V4L2_CTRL_CLASS_USER | 1 V4L2_CID_BRIGHTNESS = V4L2_CID_BASE + 0 V4L2_CID_CONTRAST = V4L2_CID_BASE + 1 V4L2_CID_SATURATION = V4L2_CID_BASE + 2 V4L2_CID_HUE = V4L2_CID_BASE + 3 V4L2_CID_AUDIO_VOLUME = V4L2_CID_BASE + 5 V4L2_CID_AUDIO_BALANCE = V4L2_CID_BASE + 6 V4L2_CID_AUDIO_BASS = V4L2_CID_BASE + 7 V4L2_CID_AUDIO_TREBLE = V4L2_CID_BASE + 8 V4L2_CID_AUDIO_MUTE = V4L2_CID_BASE + 9 V4L2_CID_AUDIO_LOUDNESS = V4L2_CID_BASE + 10 V4L2_CID_BLACK_LEVEL = V4L2_CID_BASE + 11 # Deprecated V4L2_CID_AUTO_WHITE_BALANCE = V4L2_CID_BASE + 12 V4L2_CID_DO_WHITE_BALANCE = V4L2_CID_BASE + 13 V4L2_CID_RED_BALANCE = V4L2_CID_BASE + 14 V4L2_CID_BLUE_BALANCE = V4L2_CID_BASE + 15 V4L2_CID_GAMMA = V4L2_CID_BASE + 16 V4L2_CID_WHITENESS = V4L2_CID_GAMMA # Deprecated V4L2_CID_EXPOSURE = V4L2_CID_BASE + 17 V4L2_CID_AUTOGAIN = V4L2_CID_BASE + 18 V4L2_CID_GAIN = V4L2_CID_BASE + 19 V4L2_CID_HFLIP = V4L2_CID_BASE + 20 V4L2_CID_VFLIP = V4L2_CID_BASE + 21 # Deprecated; use V4L2_CID_PAN_RESET and V4L2_CID_TILT_RESET V4L2_CID_HCENTER = V4L2_CID_BASE + 22 V4L2_CID_VCENTER = V4L2_CID_BASE + 23 V4L2_CID_POWER_LINE_FREQUENCY = V4L2_CID_BASE + 24 v4l2_power_line_frequency = enum ( V4L2_CID_POWER_LINE_FREQUENCY_DISABLED, V4L2_CID_POWER_LINE_FREQUENCY_50HZ, V4L2_CID_POWER_LINE_FREQUENCY_60HZ, ) = range(3) V4L2_CID_HUE_AUTO = V4L2_CID_BASE + 25 V4L2_CID_WHITE_BALANCE_TEMPERATURE = V4L2_CID_BASE + 26 V4L2_CID_SHARPNESS = V4L2_CID_BASE + 27 V4L2_CID_BACKLIGHT_COMPENSATION = V4L2_CID_BASE + 28 V4L2_CID_CHROMA_AGC = V4L2_CID_BASE + 29 V4L2_CID_COLOR_KILLER = V4L2_CID_BASE + 30 V4L2_CID_COLORFX = V4L2_CID_BASE + 31 v4l2_colorfx = enum ( V4L2_COLORFX_NONE, V4L2_COLORFX_BW, V4L2_COLORFX_SEPIA, ) = range(3) V4L2_CID_AUTOBRIGHTNESS = V4L2_CID_BASE + 32 V4L2_CID_BAND_STOP_FILTER = V4L2_CID_BASE + 33 V4L2_CID_ROTATE = V4L2_CID_BASE + 34 V4L2_CID_BG_COLOR = V4L2_CID_BASE + 35 V4L2_CID_LASTP1 = V4L2_CID_BASE + 36 V4L2_CID_MPEG_BASE = V4L2_CTRL_CLASS_MPEG | 0x900 V4L2_CID_MPEG_CLASS = V4L2_CTRL_CLASS_MPEG | 1 # MPEG streams V4L2_CID_MPEG_STREAM_TYPE = V4L2_CID_MPEG_BASE + 0 v4l2_mpeg_stream_type = enum ( V4L2_MPEG_STREAM_TYPE_MPEG2_PS, V4L2_MPEG_STREAM_TYPE_MPEG2_TS, V4L2_MPEG_STREAM_TYPE_MPEG1_SS, V4L2_MPEG_STREAM_TYPE_MPEG2_DVD, V4L2_MPEG_STREAM_TYPE_MPEG1_VCD, V4L2_MPEG_STREAM_TYPE_MPEG2_SVCD, ) = range(6) V4L2_CID_MPEG_STREAM_PID_PMT = V4L2_CID_MPEG_BASE + 1 V4L2_CID_MPEG_STREAM_PID_AUDIO = V4L2_CID_MPEG_BASE + 2 V4L2_CID_MPEG_STREAM_PID_VIDEO = V4L2_CID_MPEG_BASE + 3 V4L2_CID_MPEG_STREAM_PID_PCR = V4L2_CID_MPEG_BASE + 4 V4L2_CID_MPEG_STREAM_PES_ID_AUDIO = V4L2_CID_MPEG_BASE + 5 V4L2_CID_MPEG_STREAM_PES_ID_VIDEO = V4L2_CID_MPEG_BASE + 6 V4L2_CID_MPEG_STREAM_VBI_FMT = V4L2_CID_MPEG_BASE + 7 v4l2_mpeg_stream_vbi_fmt = enum ( V4L2_MPEG_STREAM_VBI_FMT_NONE, V4L2_MPEG_STREAM_VBI_FMT_IVTV, ) = range(2) V4L2_CID_MPEG_AUDIO_SAMPLING_FREQ = V4L2_CID_MPEG_BASE + 100 v4l2_mpeg_audio_sampling_freq = enum ( V4L2_MPEG_AUDIO_SAMPLING_FREQ_44100, V4L2_MPEG_AUDIO_SAMPLING_FREQ_48000, V4L2_MPEG_AUDIO_SAMPLING_FREQ_32000, ) = range(3) V4L2_CID_MPEG_AUDIO_ENCODING = V4L2_CID_MPEG_BASE + 101 v4l2_mpeg_audio_encoding = enum ( V4L2_MPEG_AUDIO_ENCODING_LAYER_1, V4L2_MPEG_AUDIO_ENCODING_LAYER_2, V4L2_MPEG_AUDIO_ENCODING_LAYER_3, V4L2_MPEG_AUDIO_ENCODING_AAC, V4L2_MPEG_AUDIO_ENCODING_AC3, ) = range(5) V4L2_CID_MPEG_AUDIO_L1_BITRATE = V4L2_CID_MPEG_BASE + 102 v4l2_mpeg_audio_l1_bitrate = enum ( V4L2_MPEG_AUDIO_L1_BITRATE_32K, V4L2_MPEG_AUDIO_L1_BITRATE_64K, V4L2_MPEG_AUDIO_L1_BITRATE_96K, V4L2_MPEG_AUDIO_L1_BITRATE_128K, V4L2_MPEG_AUDIO_L1_BITRATE_160K, V4L2_MPEG_AUDIO_L1_BITRATE_192K, V4L2_MPEG_AUDIO_L1_BITRATE_224K, V4L2_MPEG_AUDIO_L1_BITRATE_256K, V4L2_MPEG_AUDIO_L1_BITRATE_288K, V4L2_MPEG_AUDIO_L1_BITRATE_320K, V4L2_MPEG_AUDIO_L1_BITRATE_352K, V4L2_MPEG_AUDIO_L1_BITRATE_384K, V4L2_MPEG_AUDIO_L1_BITRATE_416K, V4L2_MPEG_AUDIO_L1_BITRATE_448K, ) = range(14) V4L2_CID_MPEG_AUDIO_L2_BITRATE = V4L2_CID_MPEG_BASE + 103 v4l2_mpeg_audio_l2_bitrate = enum ( V4L2_MPEG_AUDIO_L2_BITRATE_32K, V4L2_MPEG_AUDIO_L2_BITRATE_48K, V4L2_MPEG_AUDIO_L2_BITRATE_56K, V4L2_MPEG_AUDIO_L2_BITRATE_64K, V4L2_MPEG_AUDIO_L2_BITRATE_80K, V4L2_MPEG_AUDIO_L2_BITRATE_96K, V4L2_MPEG_AUDIO_L2_BITRATE_112K, V4L2_MPEG_AUDIO_L2_BITRATE_128K, V4L2_MPEG_AUDIO_L2_BITRATE_160K, V4L2_MPEG_AUDIO_L2_BITRATE_192K, V4L2_MPEG_AUDIO_L2_BITRATE_224K, V4L2_MPEG_AUDIO_L2_BITRATE_256K, V4L2_MPEG_AUDIO_L2_BITRATE_320K, V4L2_MPEG_AUDIO_L2_BITRATE_384K, ) = range(14) V4L2_CID_MPEG_AUDIO_L3_BITRATE = V4L2_CID_MPEG_BASE + 104 v4l2_mpeg_audio_l3_bitrate = enum ( V4L2_MPEG_AUDIO_L3_BITRATE_32K, V4L2_MPEG_AUDIO_L3_BITRATE_40K, V4L2_MPEG_AUDIO_L3_BITRATE_48K, V4L2_MPEG_AUDIO_L3_BITRATE_56K, V4L2_MPEG_AUDIO_L3_BITRATE_64K, V4L2_MPEG_AUDIO_L3_BITRATE_80K, V4L2_MPEG_AUDIO_L3_BITRATE_96K, V4L2_MPEG_AUDIO_L3_BITRATE_112K, V4L2_MPEG_AUDIO_L3_BITRATE_128K, V4L2_MPEG_AUDIO_L3_BITRATE_160K, V4L2_MPEG_AUDIO_L3_BITRATE_192K, V4L2_MPEG_AUDIO_L3_BITRATE_224K, V4L2_MPEG_AUDIO_L3_BITRATE_256K, V4L2_MPEG_AUDIO_L3_BITRATE_320K, ) = range(14) V4L2_CID_MPEG_AUDIO_MODE = V4L2_CID_MPEG_BASE + 105 v4l2_mpeg_audio_mode = enum ( V4L2_MPEG_AUDIO_MODE_STEREO, V4L2_MPEG_AUDIO_MODE_JOINT_STEREO, V4L2_MPEG_AUDIO_MODE_DUAL, V4L2_MPEG_AUDIO_MODE_MONO, ) = range(4) V4L2_CID_MPEG_AUDIO_MODE_EXTENSION = V4L2_CID_MPEG_BASE + 106 v4l2_mpeg_audio_mode_extension = enum ( V4L2_MPEG_AUDIO_MODE_EXTENSION_BOUND_4, V4L2_MPEG_AUDIO_MODE_EXTENSION_BOUND_8, V4L2_MPEG_AUDIO_MODE_EXTENSION_BOUND_12, V4L2_MPEG_AUDIO_MODE_EXTENSION_BOUND_16, ) = range(4) V4L2_CID_MPEG_AUDIO_EMPHASIS = V4L2_CID_MPEG_BASE + 107 v4l2_mpeg_audio_emphasis = enum ( V4L2_MPEG_AUDIO_EMPHASIS_NONE, V4L2_MPEG_AUDIO_EMPHASIS_50_DIV_15_uS, V4L2_MPEG_AUDIO_EMPHASIS_CCITT_J17, ) = range(3) V4L2_CID_MPEG_AUDIO_CRC = V4L2_CID_MPEG_BASE + 108 v4l2_mpeg_audio_crc = enum ( V4L2_MPEG_AUDIO_CRC_NONE, V4L2_MPEG_AUDIO_CRC_CRC16, ) = range(2) V4L2_CID_MPEG_AUDIO_MUTE = V4L2_CID_MPEG_BASE + 109 V4L2_CID_MPEG_AUDIO_AAC_BITRATE = V4L2_CID_MPEG_BASE + 110 V4L2_CID_MPEG_AUDIO_AC3_BITRATE = V4L2_CID_MPEG_BASE + 111 v4l2_mpeg_audio_ac3_bitrate = enum ( V4L2_MPEG_AUDIO_AC3_BITRATE_32K, V4L2_MPEG_AUDIO_AC3_BITRATE_40K, V4L2_MPEG_AUDIO_AC3_BITRATE_48K, V4L2_MPEG_AUDIO_AC3_BITRATE_56K, V4L2_MPEG_AUDIO_AC3_BITRATE_64K, V4L2_MPEG_AUDIO_AC3_BITRATE_80K, V4L2_MPEG_AUDIO_AC3_BITRATE_96K, V4L2_MPEG_AUDIO_AC3_BITRATE_112K, V4L2_MPEG_AUDIO_AC3_BITRATE_128K, V4L2_MPEG_AUDIO_AC3_BITRATE_160K, V4L2_MPEG_AUDIO_AC3_BITRATE_192K, V4L2_MPEG_AUDIO_AC3_BITRATE_224K, V4L2_MPEG_AUDIO_AC3_BITRATE_256K, V4L2_MPEG_AUDIO_AC3_BITRATE_320K, V4L2_MPEG_AUDIO_AC3_BITRATE_384K, V4L2_MPEG_AUDIO_AC3_BITRATE_448K, V4L2_MPEG_AUDIO_AC3_BITRATE_512K, V4L2_MPEG_AUDIO_AC3_BITRATE_576K, V4L2_MPEG_AUDIO_AC3_BITRATE_640K, ) = range(19) V4L2_CID_MPEG_VIDEO_ENCODING = V4L2_CID_MPEG_BASE + 200 v4l2_mpeg_video_encoding = enum ( V4L2_MPEG_VIDEO_ENCODING_MPEG_1, V4L2_MPEG_VIDEO_ENCODING_MPEG_2, V4L2_MPEG_VIDEO_ENCODING_MPEG_4_AVC, ) = range(3) V4L2_CID_MPEG_VIDEO_ASPECT = V4L2_CID_MPEG_BASE + 201 v4l2_mpeg_video_aspect = enum ( V4L2_MPEG_VIDEO_ASPECT_1x1, V4L2_MPEG_VIDEO_ASPECT_4x3, V4L2_MPEG_VIDEO_ASPECT_16x9, V4L2_MPEG_VIDEO_ASPECT_221x100, ) = range(4) V4L2_CID_MPEG_VIDEO_B_FRAMES = V4L2_CID_MPEG_BASE + 202 V4L2_CID_MPEG_VIDEO_GOP_SIZE = V4L2_CID_MPEG_BASE + 203 V4L2_CID_MPEG_VIDEO_GOP_CLOSURE = V4L2_CID_MPEG_BASE + 204 V4L2_CID_MPEG_VIDEO_PULLDOWN = V4L2_CID_MPEG_BASE + 205 V4L2_CID_MPEG_VIDEO_BITRATE_MODE = V4L2_CID_MPEG_BASE + 206 v4l2_mpeg_video_bitrate_mode = enum ( V4L2_MPEG_VIDEO_BITRATE_MODE_VBR, V4L2_MPEG_VIDEO_BITRATE_MODE_CBR, ) = range(2) V4L2_CID_MPEG_VIDEO_BITRATE = V4L2_CID_MPEG_BASE + 207 V4L2_CID_MPEG_VIDEO_BITRATE_PEAK = V4L2_CID_MPEG_BASE + 208 V4L2_CID_MPEG_VIDEO_TEMPORAL_DECIMATION = V4L2_CID_MPEG_BASE + 209 V4L2_CID_MPEG_VIDEO_MUTE = V4L2_CID_MPEG_BASE + 210 V4L2_CID_MPEG_VIDEO_MUTE_YUV = V4L2_CID_MPEG_BASE + 211 V4L2_CID_MPEG_CX2341X_BASE = V4L2_CTRL_CLASS_MPEG | 0x1000 V4L2_CID_MPEG_CX2341X_VIDEO_SPATIAL_FILTER_MODE = V4L2_CID_MPEG_CX2341X_BASE + 0 v4l2_mpeg_cx2341x_video_spatial_filter_mode = enum ( V4L2_MPEG_CX2341X_VIDEO_SPATIAL_FILTER_MODE_MANUAL, V4L2_MPEG_CX2341X_VIDEO_SPATIAL_FILTER_MODE_AUTO, ) = range(2) V4L2_CID_MPEG_CX2341X_VIDEO_SPATIAL_FILTER = V4L2_CID_MPEG_CX2341X_BASE + 1 V4L2_CID_MPEG_CX2341X_VIDEO_LUMA_SPATIAL_FILTER_TYPE = V4L2_CID_MPEG_CX2341X_BASE + 2 v4l2_mpeg_cx2341x_video_luma_spatial_filter_type = enum ( V4L2_MPEG_CX2341X_VIDEO_LUMA_SPATIAL_FILTER_TYPE_OFF, V4L2_MPEG_CX2341X_VIDEO_LUMA_SPATIAL_FILTER_TYPE_1D_HOR, V4L2_MPEG_CX2341X_VIDEO_LUMA_SPATIAL_FILTER_TYPE_1D_VERT, V4L2_MPEG_CX2341X_VIDEO_LUMA_SPATIAL_FILTER_TYPE_2D_HV_SEPARABLE, V4L2_MPEG_CX2341X_VIDEO_LUMA_SPATIAL_FILTER_TYPE_2D_SYM_NON_SEPARABLE, ) = range(5) V4L2_CID_MPEG_CX2341X_VIDEO_CHROMA_SPATIAL_FILTER_TYPE = V4L2_CID_MPEG_CX2341X_BASE + 3 v4l2_mpeg_cx2341x_video_chroma_spatial_filter_type = enum ( V4L2_MPEG_CX2341X_VIDEO_CHROMA_SPATIAL_FILTER_TYPE_OFF, V4L2_MPEG_CX2341X_VIDEO_CHROMA_SPATIAL_FILTER_TYPE_1D_HOR, ) = range(2) V4L2_CID_MPEG_CX2341X_VIDEO_TEMPORAL_FILTER_MODE = V4L2_CID_MPEG_CX2341X_BASE + 4 v4l2_mpeg_cx2341x_video_temporal_filter_mode = enum ( V4L2_MPEG_CX2341X_VIDEO_TEMPORAL_FILTER_MODE_MANUAL, V4L2_MPEG_CX2341X_VIDEO_TEMPORAL_FILTER_MODE_AUTO, ) = range(2) V4L2_CID_MPEG_CX2341X_VIDEO_TEMPORAL_FILTER = V4L2_CID_MPEG_CX2341X_BASE + 5 V4L2_CID_MPEG_CX2341X_VIDEO_MEDIAN_FILTER_TYPE = V4L2_CID_MPEG_CX2341X_BASE + 6 v4l2_mpeg_cx2341x_video_median_filter_type = enum ( V4L2_MPEG_CX2341X_VIDEO_MEDIAN_FILTER_TYPE_OFF, V4L2_MPEG_CX2341X_VIDEO_MEDIAN_FILTER_TYPE_HOR, V4L2_MPEG_CX2341X_VIDEO_MEDIAN_FILTER_TYPE_VERT, V4L2_MPEG_CX2341X_VIDEO_MEDIAN_FILTER_TYPE_HOR_VERT, V4L2_MPEG_CX2341X_VIDEO_MEDIAN_FILTER_TYPE_DIAG, ) = range(5) V4L2_CID_MPEG_CX2341X_VIDEO_LUMA_MEDIAN_FILTER_BOTTOM = V4L2_CID_MPEG_CX2341X_BASE + 7 V4L2_CID_MPEG_CX2341X_VIDEO_LUMA_MEDIAN_FILTER_TOP = V4L2_CID_MPEG_CX2341X_BASE + 8 V4L2_CID_MPEG_CX2341X_VIDEO_CHROMA_MEDIAN_FILTER_BOTTOM = V4L2_CID_MPEG_CX2341X_BASE + 9 V4L2_CID_MPEG_CX2341X_VIDEO_CHROMA_MEDIAN_FILTER_TOP = V4L2_CID_MPEG_CX2341X_BASE + 10 V4L2_CID_MPEG_CX2341X_STREAM_INSERT_NAV_PACKETS = V4L2_CID_MPEG_CX2341X_BASE + 11 V4L2_CID_CAMERA_CLASS_BASE = V4L2_CTRL_CLASS_CAMERA | 0x900 V4L2_CID_CAMERA_CLASS = V4L2_CTRL_CLASS_CAMERA | 1 V4L2_CID_EXPOSURE_AUTO = V4L2_CID_CAMERA_CLASS_BASE + 1 v4l2_exposure_auto_type = enum ( V4L2_EXPOSURE_AUTO, V4L2_EXPOSURE_MANUAL, V4L2_EXPOSURE_SHUTTER_PRIORITY, V4L2_EXPOSURE_APERTURE_PRIORITY, ) = range(4) V4L2_CID_EXPOSURE_ABSOLUTE = V4L2_CID_CAMERA_CLASS_BASE + 2 V4L2_CID_EXPOSURE_AUTO_PRIORITY = V4L2_CID_CAMERA_CLASS_BASE + 3 V4L2_CID_PAN_RELATIVE = V4L2_CID_CAMERA_CLASS_BASE + 4 V4L2_CID_TILT_RELATIVE = V4L2_CID_CAMERA_CLASS_BASE + 5 V4L2_CID_PAN_RESET = V4L2_CID_CAMERA_CLASS_BASE + 6 V4L2_CID_TILT_RESET = V4L2_CID_CAMERA_CLASS_BASE + 7 V4L2_CID_PAN_ABSOLUTE = V4L2_CID_CAMERA_CLASS_BASE + 8 V4L2_CID_TILT_ABSOLUTE = V4L2_CID_CAMERA_CLASS_BASE + 9 V4L2_CID_FOCUS_ABSOLUTE = V4L2_CID_CAMERA_CLASS_BASE + 10 V4L2_CID_FOCUS_RELATIVE = V4L2_CID_CAMERA_CLASS_BASE + 11 V4L2_CID_FOCUS_AUTO = V4L2_CID_CAMERA_CLASS_BASE + 12 V4L2_CID_ZOOM_ABSOLUTE = V4L2_CID_CAMERA_CLASS_BASE + 13 V4L2_CID_ZOOM_RELATIVE = V4L2_CID_CAMERA_CLASS_BASE + 14 V4L2_CID_ZOOM_CONTINUOUS = V4L2_CID_CAMERA_CLASS_BASE + 15 V4L2_CID_PRIVACY = V4L2_CID_CAMERA_CLASS_BASE + 16 V4L2_CID_FM_TX_CLASS_BASE = V4L2_CTRL_CLASS_FM_TX | 0x900 V4L2_CID_FM_TX_CLASS = V4L2_CTRL_CLASS_FM_TX | 1 V4L2_CID_RDS_TX_DEVIATION = V4L2_CID_FM_TX_CLASS_BASE + 1 V4L2_CID_RDS_TX_PI = V4L2_CID_FM_TX_CLASS_BASE + 2 V4L2_CID_RDS_TX_PTY = V4L2_CID_FM_TX_CLASS_BASE + 3 V4L2_CID_RDS_TX_PS_NAME = V4L2_CID_FM_TX_CLASS_BASE + 5 V4L2_CID_RDS_TX_RADIO_TEXT = V4L2_CID_FM_TX_CLASS_BASE + 6 V4L2_CID_AUDIO_LIMITER_ENABLED = V4L2_CID_FM_TX_CLASS_BASE + 64 V4L2_CID_AUDIO_LIMITER_RELEASE_TIME = V4L2_CID_FM_TX_CLASS_BASE + 65 V4L2_CID_AUDIO_LIMITER_DEVIATION = V4L2_CID_FM_TX_CLASS_BASE + 66 V4L2_CID_AUDIO_COMPRESSION_ENABLED = V4L2_CID_FM_TX_CLASS_BASE + 80 V4L2_CID_AUDIO_COMPRESSION_GAIN = V4L2_CID_FM_TX_CLASS_BASE + 81 V4L2_CID_AUDIO_COMPRESSION_THRESHOLD = V4L2_CID_FM_TX_CLASS_BASE + 82 V4L2_CID_AUDIO_COMPRESSION_ATTACK_TIME = V4L2_CID_FM_TX_CLASS_BASE + 83 V4L2_CID_AUDIO_COMPRESSION_RELEASE_TIME = V4L2_CID_FM_TX_CLASS_BASE + 84 V4L2_CID_PILOT_TONE_ENABLED = V4L2_CID_FM_TX_CLASS_BASE + 96 V4L2_CID_PILOT_TONE_DEVIATION = V4L2_CID_FM_TX_CLASS_BASE + 97 V4L2_CID_PILOT_TONE_FREQUENCY = V4L2_CID_FM_TX_CLASS_BASE + 98 V4L2_CID_TUNE_PREEMPHASIS = V4L2_CID_FM_TX_CLASS_BASE + 112 v4l2_preemphasis = enum ( V4L2_PREEMPHASIS_DISABLED, V4L2_PREEMPHASIS_50_uS, V4L2_PREEMPHASIS_75_uS, ) = range(3) V4L2_CID_TUNE_POWER_LEVEL = V4L2_CID_FM_TX_CLASS_BASE + 113 V4L2_CID_TUNE_ANTENNA_CAPACITOR = V4L2_CID_FM_TX_CLASS_BASE + 114 # # Tuning # class v4l2_tuner(ctypes.Structure): _fields_ = [ ('index', ctypes.c_uint32), ('name', ctypes.c_char * 32), ('type', v4l2_tuner_type), ('capability', ctypes.c_uint32), ('rangelow', ctypes.c_uint32), ('rangehigh', ctypes.c_uint32), ('rxsubchans', ctypes.c_uint32), ('audmode', ctypes.c_uint32), ('signal', ctypes.c_int32), ('afc', ctypes.c_int32), ('reserved', ctypes.c_uint32 * 4), ] class v4l2_modulator(ctypes.Structure): _fields_ = [ ('index', ctypes.c_uint32), ('name', ctypes.c_char * 32), ('capability', ctypes.c_uint32), ('rangelow', ctypes.c_uint32), ('rangehigh', ctypes.c_uint32), ('txsubchans', ctypes.c_uint32), ('reserved', ctypes.c_uint32 * 4), ] V4L2_TUNER_CAP_LOW = 0x0001 V4L2_TUNER_CAP_NORM = 0x0002 V4L2_TUNER_CAP_STEREO = 0x0010 V4L2_TUNER_CAP_LANG2 = 0x0020 V4L2_TUNER_CAP_SAP = 0x0020 V4L2_TUNER_CAP_LANG1 = 0x0040 V4L2_TUNER_CAP_RDS = 0x0080 V4L2_TUNER_SUB_MONO = 0x0001 V4L2_TUNER_SUB_STEREO = 0x0002 V4L2_TUNER_SUB_LANG2 = 0x0004 V4L2_TUNER_SUB_SAP = 0x0004 V4L2_TUNER_SUB_LANG1 = 0x0008 V4L2_TUNER_SUB_RDS = 0x0010 V4L2_TUNER_MODE_MONO = 0x0000 V4L2_TUNER_MODE_STEREO = 0x0001 V4L2_TUNER_MODE_LANG2 = 0x0002 V4L2_TUNER_MODE_SAP = 0x0002 V4L2_TUNER_MODE_LANG1 = 0x0003 V4L2_TUNER_MODE_LANG1_LANG2 = 0x0004 class v4l2_frequency(ctypes.Structure): _fields_ = [ ('tuner', ctypes.c_uint32), ('type', v4l2_tuner_type), ('frequency', ctypes.c_uint32), ('reserved', ctypes.c_uint32 * 8), ] class v4l2_hw_freq_seek(ctypes.Structure): _fields_ = [ ('tuner', ctypes.c_uint32), ('type', v4l2_tuner_type), ('seek_upward', ctypes.c_uint32), ('wrap_around', ctypes.c_uint32), ('reserved', ctypes.c_uint32 * 8), ] # # RDS # class v4l2_rds_data(ctypes.Structure): _fields_ = [ ('lsb', ctypes.c_char), ('msb', ctypes.c_char), ('block', ctypes.c_char), ] _pack_ = True V4L2_RDS_BLOCK_MSK = 0x7 V4L2_RDS_BLOCK_A = 0 V4L2_RDS_BLOCK_B = 1 V4L2_RDS_BLOCK_C = 2 V4L2_RDS_BLOCK_D = 3 V4L2_RDS_BLOCK_C_ALT = 4 V4L2_RDS_BLOCK_INVALID = 7 V4L2_RDS_BLOCK_CORRECTED = 0x40 V4L2_RDS_BLOCK_ERROR = 0x80 # # Audio # class v4l2_audio(ctypes.Structure): _fields_ = [ ('index', ctypes.c_uint32), ('name', ctypes.c_char * 32), ('capability', ctypes.c_uint32), ('mode', ctypes.c_uint32), ('reserved', ctypes.c_uint32 * 2), ] V4L2_AUDCAP_STEREO = 0x00001 V4L2_AUDCAP_AVL = 0x00002 V4L2_AUDMODE_AVL = 0x00001 class v4l2_audioout(ctypes.Structure): _fields_ = [ ('index', ctypes.c_uint32), ('name', ctypes.c_char * 32), ('capability', ctypes.c_uint32), ('mode', ctypes.c_uint32), ('reserved', ctypes.c_uint32 * 2), ] # # Mpeg services (experimental) # V4L2_ENC_IDX_FRAME_I = 0 V4L2_ENC_IDX_FRAME_P = 1 V4L2_ENC_IDX_FRAME_B = 2 V4L2_ENC_IDX_FRAME_MASK = 0xf class v4l2_enc_idx_entry(ctypes.Structure): _fields_ = [ ('offset', ctypes.c_uint64), ('pts', ctypes.c_uint64), ('length', ctypes.c_uint32), ('flags', ctypes.c_uint32), ('reserved', ctypes.c_uint32 * 2), ] V4L2_ENC_IDX_ENTRIES = 64 class v4l2_enc_idx(ctypes.Structure): _fields_ = [ ('entries', ctypes.c_uint32), ('entries_cap', ctypes.c_uint32), ('reserved', ctypes.c_uint32 * 4), ('entry', v4l2_enc_idx_entry * V4L2_ENC_IDX_ENTRIES), ] V4L2_ENC_CMD_START = 0 V4L2_ENC_CMD_STOP = 1 V4L2_ENC_CMD_PAUSE = 2 V4L2_ENC_CMD_RESUME = 3 V4L2_ENC_CMD_STOP_AT_GOP_END = 1 << 0 class v4l2_encoder_cmd(ctypes.Structure): class _u(ctypes.Union): class _s(ctypes.Structure): _fields_ = [ ('data', ctypes.c_uint32 * 8), ] _fields_ = [ ('raw', _s), ] _fields_ = [ ('cmd', ctypes.c_uint32), ('flags', ctypes.c_uint32), ('_u', _u), ] _anonymous_ = ('_u',) # # Data services (VBI) # class v4l2_vbi_format(ctypes.Structure): _fields_ = [ ('sampling_rate', ctypes.c_uint32), ('offset', ctypes.c_uint32), ('samples_per_line', ctypes.c_uint32), ('sample_format', ctypes.c_uint32), ('start', ctypes.c_int32 * 2), ('count', ctypes.c_uint32 * 2), ('flags', ctypes.c_uint32), ('reserved', ctypes.c_uint32 * 2), ] V4L2_VBI_UNSYNC = 1 << 0 V4L2_VBI_INTERLACED = 1 << 1 class v4l2_sliced_vbi_format(ctypes.Structure): _fields_ = [ ('service_set', ctypes.c_uint16), ('service_lines', ctypes.c_uint16 * 2 * 24), ('io_size', ctypes.c_uint32), ('reserved', ctypes.c_uint32 * 2), ] V4L2_SLICED_TELETEXT_B = 0x0001 V4L2_SLICED_VPS = 0x0400 V4L2_SLICED_CAPTION_525 = 0x1000 V4L2_SLICED_WSS_625 = 0x4000 V4L2_SLICED_VBI_525 = V4L2_SLICED_CAPTION_525 V4L2_SLICED_VBI_625 = ( V4L2_SLICED_TELETEXT_B | V4L2_SLICED_VPS | V4L2_SLICED_WSS_625) class v4l2_sliced_vbi_cap(ctypes.Structure): _fields_ = [ ('service_set', ctypes.c_uint16), ('service_lines', ctypes.c_uint16 * 2 * 24), ('type', v4l2_buf_type), ('reserved', ctypes.c_uint32 * 3), ] class v4l2_sliced_vbi_data(ctypes.Structure): _fields_ = [ ('id', ctypes.c_uint32), ('field', ctypes.c_uint32), ('line', ctypes.c_uint32), ('reserved', ctypes.c_uint32), ('data', ctypes.c_char * 48), ] # # Sliced VBI data inserted into MPEG Streams # V4L2_MPEG_VBI_IVTV_TELETEXT_B = 1 V4L2_MPEG_VBI_IVTV_CAPTION_525 = 4 V4L2_MPEG_VBI_IVTV_WSS_625 = 5 V4L2_MPEG_VBI_IVTV_VPS = 7 class v4l2_mpeg_vbi_itv0_line(ctypes.Structure): _fields_ = [ ('id', ctypes.c_char), ('data', ctypes.c_char * 42), ] _pack_ = True class v4l2_mpeg_vbi_itv0(ctypes.Structure): _fields_ = [ ('linemask', ctypes.c_uint32 * 2), # how to define __le32 in ctypes? ('line', v4l2_mpeg_vbi_itv0_line * 35), ] _pack_ = True class v4l2_mpeg_vbi_ITV0(ctypes.Structure): _fields_ = [ ('line', v4l2_mpeg_vbi_itv0_line * 36), ] _pack_ = True V4L2_MPEG_VBI_IVTV_MAGIC0 = "itv0" V4L2_MPEG_VBI_IVTV_MAGIC1 = "ITV0" class v4l2_mpeg_vbi_fmt_ivtv(ctypes.Structure): class _u(ctypes.Union): _fields_ = [ ('itv0', v4l2_mpeg_vbi_itv0), ('ITV0', v4l2_mpeg_vbi_ITV0), ] _fields_ = [ ('magic', ctypes.c_char * 4), ('_u', _u) ] _anonymous_ = ('_u',) _pack_ = True # # Aggregate structures # class v4l2_format(ctypes.Structure): class _u(ctypes.Union): _fields_ = [ ('pix', v4l2_pix_format), ('win', v4l2_window), ('vbi', v4l2_vbi_format), ('sliced', v4l2_sliced_vbi_format), ('raw_data', ctypes.c_char * 200), ] _fields_ = [ ('type', v4l2_buf_type), ('fmt', _u), ] class v4l2_streamparm(ctypes.Structure): class _u(ctypes.Union): _fields_ = [ ('capture', v4l2_captureparm), ('output', v4l2_outputparm), ('raw_data', ctypes.c_char * 200), ] _fields_ = [ ('type', v4l2_buf_type), ('parm', _u) ] # # Advanced debugging # V4L2_CHIP_MATCH_HOST = 0 V4L2_CHIP_MATCH_I2C_DRIVER = 1 V4L2_CHIP_MATCH_I2C_ADDR = 2 V4L2_CHIP_MATCH_AC97 = 3 class v4l2_dbg_match(ctypes.Structure): class _u(ctypes.Union): _fields_ = [ ('addr', ctypes.c_uint32), ('name', ctypes.c_char * 32), ] _fields_ = [ ('type', ctypes.c_uint32), ('_u', _u), ] _anonymous_ = ('_u',) _pack_ = True class v4l2_dbg_register(ctypes.Structure): _fields_ = [ ('match', v4l2_dbg_match), ('size', ctypes.c_uint32), ('reg', ctypes.c_uint64), ('val', ctypes.c_uint64), ] _pack_ = True class v4l2_dbg_chip_ident(ctypes.Structure): _fields_ = [ ('match', v4l2_dbg_match), ('ident', ctypes.c_uint32), ('revision', ctypes.c_uint32), ] _pack_ = True # # ioctl codes for video devices # VIDIOC_QUERYCAP = _IOR('V', 0, v4l2_capability) VIDIOC_RESERVED = _IO('V', 1) VIDIOC_ENUM_FMT = _IOWR('V', 2, v4l2_fmtdesc) VIDIOC_G_FMT = _IOWR('V', 4, v4l2_format) VIDIOC_S_FMT = _IOWR('V', 5, v4l2_format) VIDIOC_REQBUFS = _IOWR('V', 8, v4l2_requestbuffers) VIDIOC_QUERYBUF = _IOWR('V', 9, v4l2_buffer) VIDIOC_G_FBUF = _IOR('V', 10, v4l2_framebuffer) VIDIOC_S_FBUF = _IOW('V', 11, v4l2_framebuffer) VIDIOC_OVERLAY = _IOW('V', 14, ctypes.c_int) VIDIOC_QBUF = _IOWR('V', 15, v4l2_buffer) VIDIOC_DQBUF = _IOWR('V', 17, v4l2_buffer) VIDIOC_STREAMON = _IOW('V', 18, ctypes.c_int) VIDIOC_STREAMOFF = _IOW('V', 19, ctypes.c_int) VIDIOC_G_PARM = _IOWR('V', 21, v4l2_streamparm) VIDIOC_S_PARM = _IOWR('V', 22, v4l2_streamparm) VIDIOC_G_STD = _IOR('V', 23, v4l2_std_id) VIDIOC_S_STD = _IOW('V', 24, v4l2_std_id) VIDIOC_ENUMSTD = _IOWR('V', 25, v4l2_standard) VIDIOC_ENUMINPUT = _IOWR('V', 26, v4l2_input) VIDIOC_G_CTRL = _IOWR('V', 27, v4l2_control) VIDIOC_S_CTRL = _IOWR('V', 28, v4l2_control) VIDIOC_G_TUNER = _IOWR('V', 29, v4l2_tuner) VIDIOC_S_TUNER = _IOW('V', 30, v4l2_tuner) VIDIOC_G_AUDIO = _IOR('V', 33, v4l2_audio) VIDIOC_S_AUDIO = _IOW('V', 34, v4l2_audio) VIDIOC_QUERYCTRL = _IOWR('V', 36, v4l2_queryctrl) VIDIOC_QUERYMENU = _IOWR('V', 37, v4l2_querymenu) VIDIOC_G_INPUT = _IOR('V', 38, ctypes.c_int) VIDIOC_S_INPUT = _IOWR('V', 39, ctypes.c_int) VIDIOC_G_OUTPUT = _IOR('V', 46, ctypes.c_int) VIDIOC_S_OUTPUT = _IOWR('V', 47, ctypes.c_int) VIDIOC_ENUMOUTPUT = _IOWR('V', 48, v4l2_output) VIDIOC_G_AUDOUT = _IOR('V', 49, v4l2_audioout) VIDIOC_S_AUDOUT = _IOW('V', 50, v4l2_audioout) VIDIOC_G_MODULATOR = _IOWR('V', 54, v4l2_modulator) VIDIOC_S_MODULATOR = _IOW('V', 55, v4l2_modulator) VIDIOC_G_FREQUENCY = _IOWR('V', 56, v4l2_frequency) VIDIOC_S_FREQUENCY = _IOW('V', 57, v4l2_frequency) VIDIOC_CROPCAP = _IOWR('V', 58, v4l2_cropcap) VIDIOC_G_CROP = _IOWR('V', 59, v4l2_crop) VIDIOC_S_CROP = _IOW('V', 60, v4l2_crop) VIDIOC_G_JPEGCOMP = _IOR('V', 61, v4l2_jpegcompression) VIDIOC_S_JPEGCOMP = _IOW('V', 62, v4l2_jpegcompression) VIDIOC_QUERYSTD = _IOR('V', 63, v4l2_std_id) VIDIOC_TRY_FMT = _IOWR('V', 64, v4l2_format) VIDIOC_ENUMAUDIO = _IOWR('V', 65, v4l2_audio) VIDIOC_ENUMAUDOUT = _IOWR('V', 66, v4l2_audioout) VIDIOC_G_PRIORITY = _IOR('V', 67, v4l2_priority) VIDIOC_S_PRIORITY = _IOW('V', 68, v4l2_priority) VIDIOC_G_SLICED_VBI_CAP = _IOWR('V', 69, v4l2_sliced_vbi_cap) VIDIOC_LOG_STATUS = _IO('V', 70) VIDIOC_G_EXT_CTRLS = _IOWR('V', 71, v4l2_ext_controls) VIDIOC_S_EXT_CTRLS = _IOWR('V', 72, v4l2_ext_controls) VIDIOC_TRY_EXT_CTRLS = _IOWR('V', 73, v4l2_ext_controls) VIDIOC_ENUM_FRAMESIZES = _IOWR('V', 74, v4l2_frmsizeenum) VIDIOC_ENUM_FRAMEINTERVALS = _IOWR('V', 75, v4l2_frmivalenum) VIDIOC_G_ENC_INDEX = _IOR('V', 76, v4l2_enc_idx) VIDIOC_ENCODER_CMD = _IOWR('V', 77, v4l2_encoder_cmd) VIDIOC_TRY_ENCODER_CMD = _IOWR('V', 78, v4l2_encoder_cmd) VIDIOC_DBG_S_REGISTER = _IOW('V', 79, v4l2_dbg_register) VIDIOC_DBG_G_REGISTER = _IOWR('V', 80, v4l2_dbg_register) VIDIOC_DBG_G_CHIP_IDENT = _IOWR('V', 81, v4l2_dbg_chip_ident) VIDIOC_S_HW_FREQ_SEEK = _IOW('V', 82, v4l2_hw_freq_seek) VIDIOC_ENUM_DV_PRESETS = _IOWR('V', 83, v4l2_dv_enum_preset) VIDIOC_S_DV_PRESET = _IOWR('V', 84, v4l2_dv_preset) VIDIOC_G_DV_PRESET = _IOWR('V', 85, v4l2_dv_preset) VIDIOC_QUERY_DV_PRESET = _IOR('V', 86, v4l2_dv_preset) VIDIOC_S_DV_TIMINGS = _IOWR('V', 87, v4l2_dv_timings) VIDIOC_G_DV_TIMINGS = _IOWR('V', 88, v4l2_dv_timings) VIDIOC_OVERLAY_OLD = _IOWR('V', 14, ctypes.c_int) VIDIOC_S_PARM_OLD = _IOW('V', 22, v4l2_streamparm) VIDIOC_S_CTRL_OLD = _IOW('V', 28, v4l2_control) VIDIOC_G_AUDIO_OLD = _IOWR('V', 33, v4l2_audio) VIDIOC_G_AUDOUT_OLD = _IOWR('V', 49, v4l2_audioout) VIDIOC_CROPCAP_OLD = _IOR('V', 58, v4l2_cropcap) BASE_VIDIOC_PRIVATE = 192 ================================================ FILE: howdy/src/recorders/video_capture.py ================================================ # Top level class for a video capture providing simplified API's for common # functions # Import required modules import configparser import cv2 import os import sys from i18n import _ # Class to provide boilerplate code to build a video recorder with the # correct settings from the config file. # # The internal recorder can be accessed with 'video_capture.internal' class VideoCapture: def __init__(self, config): """ Creates a new VideoCapture instance depending on the settings in the provided config file. Config can either be a string to the path, or a pre-setup configparser. """ # Parse config from string if needed if isinstance(config, str): self.config = configparser.ConfigParser() self.config.read(config) else: self.config = config # Check device path if not os.path.exists(self.config.get("video", "device_path")): if self.config.getboolean("video", "warn_no_device", fallback=True): print(_("Howdy could not find a camera device at the path specified in the config file.")) print(_("It is very likely that the path is not configured correctly, please edit the 'device_path' config value by running:")) print("\n\tsudo howdy config\n") sys.exit(14) # Create reader # The internal video recorder self.internal = None # The frame width self.fw = None # The frame height self.fh = None self._create_reader() # Request a frame to wake the camera up self.internal.grab() def __del__(self): """ Frees resources when destroyed """ if self is not None: try: self.internal.release() except AttributeError as err: pass def release(self): """ Release cameras """ if self is not None: self.internal.release() def read_frame(self): """ Reads a frame, returns the frame and an attempted grayscale conversion of the frame in a tuple: (frame, grayscale_frame) If the grayscale conversion fails, both items in the tuple are identical. """ # Grab a single frame of video # Don't remove ret, it doesn't work without it ret, frame = self.internal.read() if not ret: print(_("Failed to read camera specified in the 'device_path' config option, aborting")) sys.exit(14) try: # Convert from color to grayscale # First processing of frame, so frame errors show up here gsframe = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) except RuntimeError: gsframe = frame except cv2.error: print("\nAn error occurred in OpenCV\n") raise return frame, gsframe def _create_reader(self): """ Sets up the video reader instance """ recording_plugin = self.config.get("video", "recording_plugin", fallback="opencv") if recording_plugin == "ffmpeg": # Set the capture source for ffmpeg from recorders.ffmpeg_reader import ffmpeg_reader self.internal = ffmpeg_reader( self.config.get("video", "device_path"), self.config.get("video", "device_format", fallback="v4l2") ) elif recording_plugin == "pyv4l2": # Set the capture source for pyv4l2 from recorders.pyv4l2_reader import pyv4l2_reader self.internal = pyv4l2_reader( self.config.get("video", "device_path"), self.config.get("video", "device_format", fallback="v4l2") ) else: # Start video capture on the IR camera through OpenCV self.internal = cv2.VideoCapture( self.config.get("video", "device_path"), cv2.CAP_V4L ) # Set the capture frame rate # Without this the first detected (and possibly lower) frame rate is used, -1 seems to select the highest # Use 0 as a fallback to avoid breaking an existing setup, new installs should default to -1 self.fps = self.config.getint("video", "device_fps", fallback=0) if self.fps != 0: self.internal.set(cv2.CAP_PROP_FPS, self.fps) # Force MJPEG decoding if true if self.config.getboolean("video", "force_mjpeg", fallback=False): # Set a magic number, will enable MJPEG but is badly documentated self.internal.set(cv2.CAP_PROP_FOURCC, 1196444237) # Set the frame width and height if requested self.fw = self.config.getint("video", "frame_width", fallback=-1) self.fh = self.config.getint("video", "frame_height", fallback=-1) if self.fw != -1: self.internal.set(cv2.CAP_PROP_FRAME_WIDTH, self.fw) if self.fh != -1: self.internal.set(cv2.CAP_PROP_FRAME_HEIGHT, self.fh) ================================================ FILE: howdy/src/rubberstamps/__init__.py ================================================ import sys import os import re from i18n import _ from importlib.machinery import SourceFileLoader class RubberStamp: """Howdy rubber stamp""" UI_TEXT = "ui_text" UI_SUBTEXT = "ui_subtext" def set_ui_text(self, text, type=None): """Convert an ui string to input howdy-gtk understands""" typedec = "M" if type == self.UI_SUBTEXT: typedec = "S" return self.send_ui_raw(typedec + "=" + text) def send_ui_raw(self, command): """Write raw command to howdy-gtk stdin""" if self.config.getboolean("debug", "verbose_stamps", fallback=False): print("Sending command to howdy-gtk: " + command) # Add a newline because the ui reads per line command += " \n" # If we're connected to the ui if self.gtk_proc: # Send the command as bytes self.gtk_proc.stdin.write(bytearray(command.encode("utf-8"))) self.gtk_proc.stdin.flush() # Write a padding line to force the command through any buffers self.gtk_proc.stdin.write(bytearray("P=_PADDING \n".encode("utf-8"))) self.gtk_proc.stdin.flush() def execute(config, gtk_proc, opencv): verbose = config.getboolean("debug", "verbose_stamps", fallback=False) dir_path = os.path.dirname(os.path.realpath(__file__)) installed_stamps = [] # Go through each file in the rubberstamp folder for filename in os.listdir(dir_path): # Remove non-readable file or directories if not os.path.isfile(dir_path + "/" + filename): continue # Remove meta files if filename in ["__init__.py", ".gitignore"]: continue # Add the found file to the list of enabled rubberstamps installed_stamps.append(filename.split(".")[0]) if verbose: print("Installed rubberstamps: " + ", ".join(installed_stamps)) # Get the rules defined in the config raw_rules = config.get("rubberstamps", "stamp_rules") rules = raw_rules.split("\n") # Go through the rules one by one for rule in rules: rule = rule.strip() if len(rule) <= 1: continue # Parse the rule with regex regex_result = re.search("^(\w+)\s+([\w\.]+)\s+([a-z]+)(.*)?$", rule, re.IGNORECASE) # Error out if the regex did not match (invalid line) if not regex_result: print(_("Error parsing rubberstamp rule: {}").format(rule)) continue type = regex_result.group(1) # Error out if the stamp name in the rule is not a file if type not in installed_stamps: print(_("Stamp not installed: {}").format(type)) continue # Load the module from file module = SourceFileLoader(type, dir_path + "/" + type + ".py").load_module() # Try to get the class with the same name try: constructor = getattr(module, type) except AttributeError: print(_("Stamp error: Class {} not found").format(type)) continue # Init the class and set common values instance = constructor() instance.verbose = verbose instance.config = config instance.gtk_proc = gtk_proc instance.opencv = opencv # Set some opensv shorthands instance.video_capture = opencv["video_capture"] instance.face_detector = opencv["face_detector"] instance.pose_predictor = opencv["pose_predictor"] instance.clahe = opencv["clahe"] # Parse and set the 2 required options for all rubberstamps instance.options = { "timeout": float(re.sub("[a-zA-Z]", "", regex_result.group(2))), "failsafe": regex_result.group(3) != "faildeadly" } # Try to get the class do declare its other config variables try: instance.declare_config() except Exception: print(_("Internal error in rubberstamp configuration declaration:")) import traceback traceback.print_exc() continue # Split the optional arguments at the end of the rule by spaces raw_options = regex_result.group(4).split() # For each of those aoptional arguments for option in raw_options: # Get the key to the left, and the value to the right of the equal sign key, value = option.split("=") # Error out if a key has been set that was not declared by the module before if key not in instance.options: print("Unknown config option for rubberstamp " + type + ": " + key) continue # Convert the argument string to an int or float if the declared option has that type if isinstance(instance.options[key], int): value = int(value) elif isinstance(instance.options[key], float): value = float(value) instance.options[key] = value if verbose: print("Stamp \"" + type + "\" options parsed:") print(instance.options) print("Executing stamp") # Make the stamp fail by default result = False # Run the stamp code try: result = instance.run() except Exception: print(_("Internal error in rubberstamp:")) import traceback traceback.print_exc() continue if verbose: print("Stamp \"" + type + "\" returned: " + str(result)) # Abort authentication if the stamp returned false if result is False: if verbose: print("Authentication aborted by rubber stamp") sys.exit(15) # This is outside the for loop, so we've run all the rules if verbose: print("All rubberstamps processed, authentication successful") # Exit with no errors sys.exit(0) ================================================ FILE: howdy/src/rubberstamps/hotkey.py ================================================ import time import sys from i18n import _ # Import the root rubberstamp class from rubberstamps import RubberStamp class hotkey(RubberStamp): pressed_key = "none" def declare_config(self): """Set the default values for the optional arguments""" self.options["abort_key"] = "esc" self.options["confirm_key"] = "enter" def run(self): """Wait for the user to press a hotkey""" time_left = self.options["timeout"] time_string = _("Aborting authorisation in {}") if self.options["failsafe"] else _("Authorising in {}") # Set the ui to default strings self.set_ui_text(time_string.format(int(time_left)), self.UI_TEXT) self.set_ui_text(_("Press {abort_key} to abort, {confirm_key} to authorise").format(abort_key=self.options["abort_key"], confirm_key=self.options["confirm_key"]), self.UI_SUBTEXT) # Try to import the keyboard module and tell the user to install the module if that fails try: import keyboard except Exception: print("\nMissing module for rubber stamp keyboard!") print("Please run:") print("\t pip3 install keyboard") sys.exit(1) # Register hotkeys with the kernel keyboard.add_hotkey(self.options["abort_key"], self.on_key, args=["abort"]) keyboard.add_hotkey(self.options["confirm_key"], self.on_key, args=["confirm"]) # While we have not hit our timeout yet while time_left > 0: # Remove 0.1 seconds from the timer, as that's how long we sleep time_left -= 0.1 # Update the ui with the new time self.set_ui_text(time_string.format(str(int(time_left) + 1)), self.UI_TEXT) # If the abort key was pressed while the loop was sleeping if self.pressed_key == "abort": # Set the ui to confirm the abort self.set_ui_text(_("Authentication aborted"), self.UI_TEXT) self.set_ui_text("", self.UI_SUBTEXT) # Exit time.sleep(1) return False # If confirm has pressed, return that auth can continue elif self.pressed_key == "confirm": return True # If no key has been pressed, wait for a bit and check again time.sleep(0.1) # When our timeout hits, either abort or continue based on failsafe of faildeadly return not self.options["failsafe"] def on_key(self, type): """Called when the user presses a key""" self.pressed_key = type ================================================ FILE: howdy/src/rubberstamps/nod.py ================================================ import time from i18n import _ # Import the root rubberstamp class from rubberstamps import RubberStamp class nod(RubberStamp): def declare_config(self): """Set the default values for the optional arguments""" self.options["min_distance"] = 6 self.options["min_directions"] = 2 def run(self): """Track a users nose to see if they nod yes or no""" self.set_ui_text(_("Nod to confirm"), self.UI_TEXT) self.set_ui_text(_("Shake your head to abort"), self.UI_SUBTEXT) # Stores relative distance between the 2 eyes in the last frame # Used to calculate the distance of the nose traveled in relation to face size in the frame last_reldist = -1 # Last point the nose was at last_nosepoint = {"x": -1, "y": -1} # Contains booleans recording successful nods and their directions recorded_nods = {"x": [], "y": []} starttime = time.time() # Keep running the loop while we have not hit timeout yet while time.time() < starttime + self.options["timeout"]: # Read a frame from the camera ret, frame = self.video_capture.read_frame() # Apply CLAHE to get a better picture frame = self.clahe.apply(frame) # Detect all faces in the frame face_locations = self.face_detector(frame, 1) # Only continue if exactly 1 face is visible in the frame if len(face_locations) != 1: continue # Get the position of the eyes and tip of the nose face_landmarks = self.pose_predictor(frame, face_locations[0]) # Calculate the relative distance between the 2 eyes reldist = face_landmarks.part(0).x - face_landmarks.part(2).x # Average this out with the distance found in the last frame to smooth it out avg_reldist = (last_reldist + reldist) / 2 # Calculate horizontal movement (shaking head) and vertical movement (nodding) for axis in ["x", "y"]: # Get the location of the nose on the active axis nosepoint = getattr(face_landmarks.part(4), axis) # If this is the first frame set the previous values to the current ones if last_nosepoint[axis] == -1: last_nosepoint[axis] = nosepoint last_reldist = reldist mindist = self.options["min_distance"] # Get the relative movement by taking the distance traveled and dividing it by eye distance movement = (nosepoint - last_nosepoint[axis]) * 100 / max(avg_reldist, 1) # If the movement is over the minimal distance threshold if movement < -mindist or movement > mindist: # If this is the first recorded nod, add it to the array if len(recorded_nods[axis]) == 0: recorded_nods[axis].append(movement < 0) # Otherwise, only add this nod if the previous nod with in the other direction elif recorded_nods[axis][-1] != (movement < 0): recorded_nods[axis].append(movement < 0) # Check if we have nodded enough on this axis if len(recorded_nods[axis]) >= self.options["min_directions"]: # If nodded yes, show confirmation in ui if (axis == "y"): self.set_ui_text(_("Confirmed authentication"), self.UI_TEXT) # If shaken no, show abort message else: self.set_ui_text(_("Aborted authentication"), self.UI_TEXT) # Remove subtext self.set_ui_text("", self.UI_SUBTEXT) # Return true for nodding yes and false for shaking no time.sleep(0.8) return axis == "y" # Save the relative distance and the nosepoint for next loop last_reldist = reldist last_nosepoint[axis] = nosepoint # We've fallen out of the loop, so timeout has been hit return not self.options["failsafe"] ================================================ FILE: howdy/src/snapshot.py ================================================ # Create and save snapshots of auth attempts # Import modules import cv2 import os from datetime import timezone, datetime import numpy as np import paths_factory def generate(frames, text_lines): """Generate a snapshot from given frames""" # Don't execute if no frames were given if len(frames) == 0: return # Get frame dimensions frame_height, frame_width, cc = frames[0].shape # Spread the given frames out horizontally snap = np.concatenate(frames, axis=1) # Create colors pad_color = [44, 44, 44] text_color = [255, 255, 255] # Add a gray square at the bottom of the image snap = cv2.copyMakeBorder(snap, 0, len(text_lines) * 20 + 40, 0, 0, cv2.BORDER_CONSTANT, value=pad_color) # Add the Howdy logo if there's space to do so if len(frames) > 1: # Load the logo from file logo = cv2.imread(paths_factory.logo_path()) # Calculate the position of the logo logo_y = frame_height + 20 logo_x = frame_width * len(frames) - 210 # Overlay the logo on top of the image snap[logo_y:logo_y+57, logo_x:logo_x+180] = logo # Go through each line line_number = 0 for line in text_lines: # Calculate how far the line should be from the top padding_top = frame_height + 30 + (line_number * 20) # Print the line onto the image cv2.putText(snap, line, (30, padding_top), cv2.FONT_HERSHEY_SIMPLEX, .4, text_color, 0, cv2.LINE_AA) line_number += 1 # Made sure a snapshot folder exist if not os.path.exists(paths_factory.snapshots_dir_path()): os.makedirs(paths_factory.snapshots_dir_path()) # Generate a filename based on the current time filename = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S.jpg") filepath = paths_factory.snapshot_path(filename) # Write the image to that file cv2.imwrite(filepath, snap) # Return the saved file location return filepath ================================================ FILE: howdy-gtk/bin/howdy-gtk.in ================================================ #!/bin/sh @python_path@ "@script_path@" "$@" ================================================ FILE: howdy-gtk/debian/changelog ================================================ howdy-gtk (0.0.1) xenial; urgency=medium * Initial testing release with sticky authentication ui -- boltgolt Thu, 03 Dec 2020 00:08:49 +0200 ================================================ FILE: howdy-gtk/debian/compat ================================================ 10 ================================================ FILE: howdy-gtk/debian/control ================================================ Source: howdy-gtk Section: misc Priority: optional Standards-Version: 3.9.7 Build-Depends: python, dh-python, devscripts, dh-make, debhelper, fakeroot Maintainer: boltgolt Vcs-Git: https://github.com/boltgolt/howdy Package: howdy-gtk Homepage: https://github.com/boltgolt/howdy Architecture: all Depends: ${misc:Depends}, curl|wget, python3, python3-pip, python3-dev, python-gtk2, python-gtk2-dev, cmake Description: Optional UI package for Howdy, written in Gtk ================================================ FILE: howdy-gtk/debian/copyright ================================================ MIT License Copyright (c) 2020 boltgolt Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: howdy-gtk/debian/howdy-gtk.links ================================================ /usr/lib/howdy-gtk/init.py /usr/bin/howdy-gtk ================================================ FILE: howdy-gtk/debian/howdy-gtk.lintian-overrides ================================================ # W: Don't require ugly linebreaks in last 5 chars howdy: debian-changelog-line-too-long ================================================ FILE: howdy-gtk/debian/install ================================================ src/. /usr/lib/howdy-gtk ================================================ FILE: howdy-gtk/debian/postinst ================================================ #!/bin/sh pip3 install elevate ================================================ FILE: howdy-gtk/debian/rules ================================================ #!/usr/bin/make -f DH_VERBOSE = 1 DPKG_EXPORT_BUILDFLAGS = 1 include /usr/share/dpkg/default.mk %: dh $@ ================================================ FILE: howdy-gtk/debian/source/format ================================================ 3.0 (native) ================================================ FILE: howdy-gtk/debian/source/options ================================================ tar-ignore = ".git" tar-ignore = ".gitignore" tar-ignore = ".github" tar-ignore = "README.md" tar-ignore = ".travis.yml" tar-ignore = "fedora" tar-ignore = "opensuse" tar-ignore = "archlinux" ================================================ FILE: howdy-gtk/meson.build ================================================ if meson.is_subproject() project('howdy-gtk', license: 'MIT', version: 'beta', meson_version: '>= 0.64.0') endif datadir = get_option('prefix') / get_option('datadir') / 'howdy-gtk' py_conf = configuration_data(paths_dict) py_conf.set('data_dir', datadir) py_paths = configure_file( input: 'src/paths.py.in', output: 'paths.py', configuration: py_conf, ) sources = files( 'src/authsticky.py', 'src/i18n.py', 'src/init.py', 'src/onboarding.py', 'src/paths_factory.py', 'src/tab_models.py', 'src/tab_video.py', 'src/window.py', ) py = import('python').find_installation( # modules: ['gi', 'elevate'] ) py.dependency() if get_option('install_in_site_packages') pysourcesinstalldir = join_paths(py.get_install_dir(), 'howdy-gtk') else pysourcesinstalldir = get_option('py_sources_dir') != '' ? get_option('py_sources_dir') / 'howdy-gtk' : join_paths(get_option('prefix'), get_option('libdir'), 'howdy-gtk') endif if get_option('install_in_site_packages') py.install_sources( sources, py_paths, subdir: 'howdy-gtk', install_tag: 'py_sources', ) else install_data( sources, py_paths, install_dir: pysourcesinstalldir, install_mode: 'r--r--r--', install_tag: 'py_sources', ) endif logos = files( 'src/logo.png', 'src/logo_about.png', ) install_data(logos, install_dir: datadir) interface_files = files( 'src/main.glade', 'src/onboarding.glade', ) install_data(interface_files, install_dir: datadir) cli_path = join_paths(pysourcesinstalldir, 'init.py') conf_data = configuration_data({ 'script_path': cli_path, 'python_path': py.full_path() }) bin_name = 'howdy-gtk' bin = configure_file( input: 'bin/howdy-gtk.in', output: bin_name, configuration: conf_data ) install_data( bin, install_mode: 'rwxr-xr-x', install_dir: get_option('prefix') / get_option('bindir'), install_tag: 'bin', ) if get_option('with_polkit') polkit_config = configure_file( input: 'src/polkit/com.github.boltgolt.howdy-gtk.policy.in', output: 'com.github.boltgolt.howdy-gtk.policy', configuration: {'script_path': cli_path, 'python_path': py.full_path()} ) install_data( polkit_config, install_dir: get_option('prefix') / get_option('datadir') / 'polkit-1' / 'actions', install_mode: 'rw-r--r--', install_tag: 'polkit', ) endif ================================================ FILE: howdy-gtk/src/authsticky.py ================================================ # Shows a floating window when authenticating import cairo import gi import signal import sys import paths_factory import os from i18n import _ # Make sure we have the libs we need gi.require_version("Gtk", "3.0") gi.require_version("Gdk", "3.0") # Import them from gi.repository import Gtk as gtk from gi.repository import Gdk as gdk from gi.repository import GObject as gobject # Set window size constants windowWidth = 400 windowHeight = 100 class StickyWindow(gtk.Window): # Set default messages to show in the popup message = _("Loading... ") subtext = "" def __init__(self): """Initialize the sticky window""" # Make the class a GTK window gtk.Window.__init__(self) # Get the absolute or relative path to the logo file logo_path = paths_factory.logo_path() # Create image and calculate scale size based on image size self.logo_surface = cairo.ImageSurface.create_from_png(logo_path) self.logo_ratio = float(windowHeight - 20) / float(self.logo_surface.get_height()) # Set the title of the window self.set_title(_("Howdy Authentication")) # Set a bunch of options to make the window stick and be on top of everything self.stick() self.set_gravity(gdk.Gravity.STATIC) self.set_resizable(False) self.set_keep_above(True) self.set_app_paintable(True) self.set_skip_pager_hint(True) self.set_skip_taskbar_hint(True) self.set_can_focus(False) self.set_can_default(False) self.set_focus(None) self.set_type_hint(gdk.WindowTypeHint.NOTIFICATION) self.set_decorated(False) # Listen for a window redraw self.connect("draw", self.draw) # Listen for a force close or click event and exit self.connect("destroy", self.exit) self.connect("delete_event", self.exit) self.connect("button-press-event", self.exit) self.connect("button-release-event", self.exit) # Create a GDK drawing, restricts the window size darea = gtk.DrawingArea() darea.set_size_request(windowWidth, windowHeight) self.add(darea) # Get the default screen screen = gdk.Screen.get_default() visual = screen.get_rgba_visual() self.set_visual(visual) # Move the window to the center top of the default window, where a webcam usually is self.move((screen.get_width() / 2) - (windowWidth / 2), 0) # Show window and force a resize again self.show_all() self.resize(windowWidth, windowHeight) # Add a timeout to catch input passed from compare.py gobject.timeout_add(100, self.catch_stdin) # Start GTK main loop gtk.main() def draw(self, widget, ctx): """Draw the UI""" # Change cursor to the kill icon self.get_window().set_cursor(gdk.Cursor(gdk.CursorType.PIRATE)) # Draw a semi transparent background ctx.set_source_rgba(0, 0, 0, .7) ctx.set_operator(cairo.OPERATOR_SOURCE) ctx.paint() ctx.set_operator(cairo.OPERATOR_OVER) # Position and draw the logo ctx.translate(15, 10) ctx.scale(self.logo_ratio, self.logo_ratio) ctx.set_source_surface(self.logo_surface) ctx.paint() # Calculate main message positioning, as the text is higher if there's a subtext if self.subtext: ctx.move_to(380, 145) else: ctx.move_to(380, 175) # Draw the main message ctx.set_source_rgba(255, 255, 255, .9) ctx.set_font_size(80) ctx.select_font_face("Arial", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL) ctx.show_text(self.message) # Draw the subtext if there is one if self.subtext: ctx.move_to(380, 210) ctx.set_source_rgba(230, 230, 230, .8) ctx.set_font_size(40) ctx.select_font_face("Arial", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL) ctx.show_text(self.subtext) def catch_stdin(self): """Catch input from stdin and redraw""" # Wait for a line on stdin comm = sys.stdin.readline()[:-1] # If the line is not empty if comm: # Parse a message if comm[0] == "M": self.message = comm[2:].strip() # Parse subtext if comm[0] == "S": # self.subtext += " " self.subtext = comm[2:].strip() # Redraw the ui self.queue_draw() # Fire this function again in 10ms, as we're waiting on IO in readline anyway gobject.timeout_add(10, self.catch_stdin) def exit(self, widget, context): """Cleanly exit""" gtk.main_quit() return True # Make sure we quit on a SIGINT signal.signal(signal.SIGINT, signal.SIG_DFL) # Open the GTK window window = StickyWindow() ================================================ FILE: howdy-gtk/src/i18n.py ================================================ # Support file for translations # Import modules import gettext import os # Get the right translation based on locale, falling back to base if none found translation = gettext.translation("gtk", localedir=os.path.join(os.path.dirname(__file__), "locales"), fallback=True) translation.install() # Export translation function as _ _ = translation.gettext ================================================ FILE: howdy-gtk/src/init.py ================================================ # Opens auth ui if requested, otherwise starts normal ui import sys if "--start-auth-ui" in sys.argv: import authsticky else: import window ================================================ FILE: howdy-gtk/src/main.glade ================================================ True False 5 gtk-add True False 5 gtk-add True False 5 gtk-delete False 5 Howdy Configuration center logo.png True True 2 left False True False vertical True False 10 10 10 5 True False center Showing saved models for: False False 0 True False False False 1 Add new user True True True 15 iconadduser none True False False end 3 False True 0 True False 10 10 vertical True False 10 10 False True 0 False True 1 True False 10 8 10 Add True True True iconadd 0.5899999737739563 True False True end 1 Delete True True True 11 icondelete True False True end 2 False True end 2 True False 10 10 Models 1 False True False 10 10 10 10 True False 10 vertical True False start Camera ID: False True 0 True False start True end False True 1 True False start 10 Real resolution: False True 2 True False start True False True 3 True False start 10 Used resolution: False True 4 True False start True False True 5 True False start 10 Recorder: False True 6 True False start True False True 7 False True end 0 300 300 True False True False gtk-execute 6 True True 1 1 True False Video 1 False True False center center vertical True False 20 12 logo_about.png False True 0 True False Howdy False True 1 True False 5 Facial authentication for Linux False True 2 True False center center 15 35 True False 3 <a href="https://github.com/boltgolt/howdy">Open GitHub link</a> True False False True 1 True False 3 <a href="https://www.buymeacoffee.com/boltgolt">Donate to the project</a> True False True 2 False True 3 2 True False About 2 False ================================================ FILE: howdy-gtk/src/onboarding.glade ================================================ True False 4 gtk-cancel True False 5 gtk-apply True False 4 gtk-go-forward True False 5 gtk-media-play 500 400 False Welcome to Howdy center logo.png menu center True False immediate vertical False True vertical True False 20 Setup is done! False True 0 True False 10 10 10 20 We're done! Howdy is now active on this computer. Try doing anything you would normally have to type your password for to authenticate, like running a command with sudo. You can open the Howdy Configurator later on to change more advanced settings or add additional models. Press Finish below to close this window and open the Howdy Configurator. center True False True 1 False True 0 False True vertical True False 20 Setting a certainty policy False True 0 True False 10 10 10 40 Because of changes in angles, distance, and other factors a face match is never exactly the same as the stored face model. On this page you can set how strict Howdy should be. center True False True 1 True False 60 60 vertical True True False 10 0 0.5099999904632568 True True radiobalanced True False 5 vertical True False start Fast False True 0 True False start Allows more fuzzy matches, but speeds up the scanning process greatly. True False True 1 False True 0 True True False 10 0 True True True False 5 vertical True False start Balanced False True 0 True False start Still relatively quick detection, but might not log you in when further away. True False True 1 False True 1 True True False 0 True True radiobalanced True False 5 vertical True False start Secure False False 0 True False start The slightly safer option, but will take much longer to authenticate you True False False 1 False True 2 False True 2 False True 1 False True vertical True False 20 Adding a face model False True 0 True False 10 10 10 20 To authenticate you Howdy needs to save a model of your face to recognise you. Press the Scan button below to start the facial scan. center True False True 1 True False Start face scan True True True True True 50 50 iconscan none right True True True 1 False True 2 False True 2 False True vertical True False 20 Configuring webcam False True 0 True False 10 20 Is the infrared emitter flashing ? center True False False True 1 True False spread yes True True True True True 0 no True True True True True 1 False True 2 False True 3 False True vertical True False 20 Configuring webcam False True 0 True False 10 10 10 20 Howdy will search your system automatically for any available cameras, so make sure your webcam is connected. After detection a list of usable webcams will be shown. Pick the one you want to use and click Next. center True False True 1 True False 0.89 10 10 vertical True False 15 Testing your webcams, please wait... False True 0 False True 2 False True 4 False True 10 vertical True False 20 Downloading data files False True 0 True False 10 10 10 20 Howdy needs three pre trained facial recognition datasets to be able to recognise you, which will be downloaded now. You can see the download progress below. center True False True 1 True False True False 10 10 10 Starting download... center True True 3 True True 5 True False vertical False 20 10 7 13 logo_about.png False True 0 True False 5 Welcome to Howdy! False True 1 100 True False center center 20 20 10 This wizard will walk you through the setup process and automatically configure Howdy for you. Press next to continue. center True False True 2 False True 6 True False 10 Cancel True True True 10 iconcancel True False True 0 Next True True True True True 10 iconforward none right True False True end 2 Finish setup True True True 10 iconfinish none True False True end 3 False True end 9 ================================================ FILE: howdy-gtk/src/onboarding.py ================================================ import sys import os import re import time import subprocess import paths_factory from i18n import _ from gi.repository import Gtk as gtk from gi.repository import Gdk as gdk from gi.repository import GObject as gobject from gi.repository import Pango as pango class OnboardingWindow(gtk.Window): def __init__(self): """Initialize the sticky window""" # Make the class a GTK window gtk.Window.__init__(self) self.builder = gtk.Builder() self.builder.add_from_file(paths_factory.onboarding_wireframe_path()) self.builder.connect_signals(self) self.window = self.builder.get_object("onboardingwindow") self.slidecontainer = self.builder.get_object("slidecontainer") self.nextbutton = self.builder.get_object("nextbutton") self.window.connect("destroy", self.exit) self.window.connect("delete_event", self.exit) self.slides = [ self.builder.get_object("slide0"), self.builder.get_object("slide1"), self.builder.get_object("slide2"), self.builder.get_object("slide3"), self.builder.get_object("slide4"), self.builder.get_object("slide5"), self.builder.get_object("slide6") ] self.window.show_all() self.window.resize(500, 400) self.window.current_slide = 0 # Start GTK main loop gtk.main() def go_next_slide(self, button=None): self.nextbutton.set_sensitive(False) self.slides[self.window.current_slide].hide() self.slides[self.window.current_slide + 1].show() self.window.current_slide += 1 # the shown child may have zero/wrong dimensions self.slidecontainer.queue_resize() if self.window.current_slide == 1: self.execute_slide1() elif self.window.current_slide == 2: gobject.timeout_add(10, self.execute_slide2) elif self.window.current_slide == 3: self.execute_slide3() elif self.window.current_slide == 4: self.execute_slide4() elif self.window.current_slide == 5: self.execute_slide5() elif self.window.current_slide == 6: self.execute_slide6() def execute_slide1(self): self.downloadoutputlabel = self.builder.get_object("downloadoutputlabel") eventbox = self.builder.get_object("downloadeventbox") eventbox.modify_bg(gtk.StateType.NORMAL, gdk.Color(red=0, green=0, blue=0)) # TODO: Better way to do this? if os.path.exists(paths_factory.dlib_data_dir_path() / "shape_predictor_5_face_landmarks.dat"): self.downloadoutputlabel.set_text(_("Datafiles have already been downloaded!\nClick Next to continue")) self.enable_next() return self.proc = subprocess.Popen("./install.sh", stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, cwd=paths_factory.dlib_data_dir_path()) self.download_lines = [] self.read_download_line() def read_download_line(self): line = self.proc.stdout.readline() self.download_lines.append(line.decode("utf-8")) print("install.sh output:") print(line.decode("utf-8")) if len(self.download_lines) > 10: self.download_lines.pop(0) self.downloadoutputlabel.set_text(" ".join(self.download_lines)) if line: gobject.timeout_add(10, self.read_download_line) return # Wait for the process to finish and check the status code if self.proc.wait(5) != 0: self.show_error(_("Error while downloading datafiles"), " ".join(self.download_lines)) self.downloadoutputlabel.set_text(_("Done!\nClick Next to continue")) self.enable_next() def execute_slide2(self): def is_gray(frame): for row in frame: for pixel in row: if not pixel[0] == pixel[1] == pixel[2]: return False return True try: import cv2 except Exception: self.show_error(_("Error while importing OpenCV2"), _("Try reinstalling cv2")) device_rows = [] try: device_ids = os.listdir("/dev/v4l/by-path") except Exception: self.show_error(_("No webcams found on system"), _("Please configure your camera yourself if you are sure a compatible camera is connected")) # Loop though all devices for dev in device_ids: time.sleep(.5) # The full path to the device is the default name device_path = "/dev/v4l/by-path/" + dev device_name = dev # Get the udevadm details to try to get a better name udevadm = subprocess.check_output(["udevadm info -r --query=all -n " + device_path], shell=True).decode("utf-8") # Loop though udevadm to search for a better name for line in udevadm.split("\n"): # Match it and encase it in quotes re_name = re.search('product.*=(.*)$', line, re.IGNORECASE) if re_name: device_name = re_name.group(1) capture = cv2.VideoCapture(device_path) is_open, frame = capture.read() if not is_open: device_rows.append([device_name, device_path, -9, _("No, camera can't be opened")]) continue try: if not is_gray(frame): raise Exception() except Exception: device_rows.append([device_name, device_path, -5, _("No, not an infrared camera")]) capture.release() continue device_rows.append([device_name, device_path, 5, _("Yes, compatible infrared camera")]) capture.release() device_rows = sorted(device_rows, key=lambda k: -k[2]) self.loadinglabel = self.builder.get_object("loadinglabel") self.devicelistbox = self.builder.get_object("devicelistbox") self.treeview = gtk.TreeView() self.treeview.set_vexpand(True) # Set the columns for i, column in enumerate([_("Camera identifier or path"), _("Recommended")]): cell = gtk.CellRendererText() cell.set_property("ellipsize", pango.EllipsizeMode.END) col = gtk.TreeViewColumn(column, cell, text=i) self.treeview.append_column(col) # Add the treeview self.devicelistbox.add(self.treeview) # Create a datamodel self.listmodel = gtk.ListStore(str, str, str, bool) for device in device_rows: is_gray = device[2] == 5 self.listmodel.append([device[0], device[3], device[1], is_gray]) self.treeview.set_model(self.listmodel) self.treeview.set_cursor(0) self.treeview.show() self.loadinglabel.hide() self.enable_next() def execute_slide3(self): try: import cv2 except Exception: self.show_error(_("Error while importing OpenCV2"), _("Try reinstalling cv2")) selection = self.treeview.get_selection() (listmodel, rowlist) = selection.get_selected_rows() if len(rowlist) != 1: self.show_error(_("Error selecting camera")) device_path = listmodel.get_value(listmodel.get_iter(rowlist[0]), 2) is_gray = listmodel.get_value(listmodel.get_iter(rowlist[0]), 3) if is_gray: # test if linux-enable-ir-emitter help should be displayed, # the user must click on the yes/no button which calls the method slide3_button_yes|no self.capture = cv2.VideoCapture(device_path) if not self.capture.isOpened(): self.show_error(_("The selected camera cannot be opened"), _("Try to select another one")) self.capture.read() else: # skip, the selected camera is not infrared self.go_next_slide() def slide3_button_yes(self, button): self.capture.release() self.go_next_slide() def slide3_button_no(self, button): self.capture.release() self.builder.get_object("leiestatus").set_markup(_("Please visit\nhttps://github.com/EmixamPP/linux-enable-ir-emitter\nto enable your ir emitter")) self.builder.get_object("leieyesbutton").hide() self.builder.get_object("leienobutton").hide() def execute_slide4(self): selection = self.treeview.get_selection() (listmodel, rowlist) = selection.get_selected_rows() if len(rowlist) != 1: self.show_error(_("Error selecting camera")) device_path = listmodel.get_value(listmodel.get_iter(rowlist[0]), 2) self.proc = subprocess.Popen("howdy set device_path " + device_path, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True) self.window.set_focus(self.builder.get_object("scanbutton")) def on_scanbutton_click(self, button): status = self.proc.wait(2) # if status != 0: # self.show_error(_("Error setting camera path"), _("Please set the camera path manually")) self.dialog = gtk.MessageDialog(parent=self, flags=gtk.DialogFlags.MODAL) self.dialog.set_title(_("Creating Model")) self.dialog.props.text = _("Please look directly into the camera") self.dialog.show_all() # Wait a bit to allow the user to read the dialog gobject.timeout_add(600, self.run_add) def run_add(self): status, output = subprocess.getstatusoutput(["howdy add -y"]) print("howdy add output:") print(output) self.dialog.destroy() if status != 0: self.show_error(_("Can't save face model"), output) gobject.timeout_add(10, self.go_next_slide) def execute_slide5(self): self.enable_next() def execute_slide6(self): radio_buttons = self.builder.get_object("radiobalanced").get_group() radio_selected = False radio_certanty = 5.0 for button in radio_buttons: if button.get_active(): radio_selected = gtk.Buildable.get_name(button) if not radio_selected: self.show_error(_("Error reading radio buttons")) elif radio_selected == "radiofast": radio_certanty = 4.2 elif radio_selected == "radiobalanced": radio_certanty = 3.5 elif radio_selected == "radiosecure": radio_certanty = 2.2 self.proc = subprocess.Popen("howdy set certainty " + str(radio_certanty), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True) self.nextbutton.hide() self.builder.get_object("cancelbutton").hide() finishbutton = self.builder.get_object("finishbutton") finishbutton.show() self.window.set_focus(finishbutton) status = self.proc.wait(2) if status != 0: self.show_error(_("Error setting certainty"), _("Certainty is set to the default value, Howdy setup is complete")) def enable_next(self): self.nextbutton.set_sensitive(True) self.window.set_focus(self.nextbutton) def show_error(self, error, secon=""): dialog = gtk.MessageDialog(parent=self, flags=gtk.DialogFlags.MODAL, type=gtk.MessageType.ERROR, buttons=gtk.ButtonsType.CLOSE) dialog.set_title(_("Howdy Error")) dialog.props.text = error dialog.format_secondary_text(secon) dialog.run() dialog.destroy() self.exit() def exit(self, widget=None, context=None): """Cleanly exit""" gtk.main_quit() sys.exit(0) ================================================ FILE: howdy-gtk/src/paths.py.in ================================================ from pathlib import PurePath # Define the absolute path to the config directory config_dir = PurePath("@config_dir@") # Define the absolute path to the DLib models data directory dlib_data_dir = PurePath("@dlib_data_dir@") # Define the absolute path to the Howdy user models directory user_models_dir = PurePath("@user_models_dir@") # Define the absolute path to the Howdy data directory data_dir = PurePath("@data_dir@") ================================================ FILE: howdy-gtk/src/paths_factory.py ================================================ from pathlib import PurePath import paths def config_file_path() -> str: """Return the path to the config file""" return str(paths.config_dir / "config.ini") def user_models_dir_path() -> PurePath: """Return the path to the user models directory""" return paths.user_models_dir def logo_path() -> str: """Return the path to the logo file""" return str(paths.data_dir / "logo.png") def onboarding_wireframe_path() -> str: """Return the path to the onboarding wireframe file""" return str(paths.data_dir / "onboarding.glade") def main_window_wireframe_path() -> str: """Return the path to the main window wireframe file""" return str(paths.data_dir / "main.glade") def dlib_data_dir_path() -> PurePath: """Return the path to the dlib data directory""" return paths.dlib_data_dir ================================================ FILE: howdy-gtk/src/polkit/com.github.boltgolt.howdy-gtk.policy.in ================================================ boltgolt https://github.com/boltgolt/howdy howdy-gtk Howdy interface Authentication is required to run howdy-gtk no no auth_admin @python_path@ @script_path@ true ================================================ FILE: howdy-gtk/src/tab_models.py ================================================ import subprocess import time from i18n import _ from gi.repository import Gtk as gtk from gi.repository import GObject as gobject def on_user_change(self, select): self.active_user = select.get_active_text() self.load_model_list() def on_user_add(self, button): # Open question dialog dialog = gtk.MessageDialog(parent=self, flags=gtk.DialogFlags.MODAL, type=gtk.MessageType.QUESTION, buttons=gtk.ButtonsType.OK_CANCEL) dialog.set_title(_("Confirm User Creation")) dialog.props.text = _("Please enter the username of the user you want to add to Howdy") # Create the input field entry = gtk.Entry() # Add a label to ask for a model name hbox = gtk.HBox() hbox.pack_start(gtk.Label(_("Username:")), False, 5, 5) hbox.pack_end(entry, True, True, 5) # Add the box and show the dialog dialog.vbox.pack_end(hbox, True, True, 0) dialog.show_all() # Show dialog response = dialog.run() entered_user = entry.get_text() dialog.destroy() if response == gtk.ResponseType.OK: self.userlist.append_text(entered_user) self.userlist.set_active(self.userlist.items) self.userlist.items += 1 self.active_user = entered_user self.load_model_list() def on_model_add(self, button): if self.userlist.items == 0: return # Open question dialog dialog = gtk.MessageDialog(parent=self, flags=gtk.DialogFlags.MODAL, type=gtk.MessageType.QUESTION, buttons=gtk.ButtonsType.OK_CANCEL) dialog.set_title(_("Confirm Model Creation")) dialog.props.text = _("Please enter a name for the new model, 24 characters max") # Create the input field entry = gtk.Entry() # Add a label to ask for a model name hbox = gtk.HBox() hbox.pack_start(gtk.Label(_("Model name:")), False, 5, 5) hbox.pack_end(entry, True, True, 5) # Add the box and show the dialog dialog.vbox.pack_end(hbox, True, True, 0) dialog.show_all() # Show dialog response = dialog.run() entered_name = entry.get_text() dialog.destroy() if response == gtk.ResponseType.OK: dialog = gtk.MessageDialog(parent=self, flags=gtk.DialogFlags.MODAL, buttons=gtk.ButtonsType.NONE) dialog.set_title(_("Creating Model")) dialog.props.text = _("Please look directly into the camera") dialog.show_all() # Wait a bit to allow the user to read the dialog gobject.timeout_add(600, lambda: execute_add(self, dialog, entered_name)) def execute_add(box, dialog, entered_name): status, output = subprocess.getstatusoutput(["howdy add '" + entered_name + "' -y -U " + box.active_user]) dialog.destroy() if status != 0: dialog = gtk.MessageDialog(parent=box, flags=gtk.DialogFlags.MODAL, type=gtk.MessageType.ERROR, buttons=gtk.ButtonsType.CLOSE) dialog.set_title(_("Howdy Error")) dialog.props.text = _("Error while adding model, error code {}: \n\n").format(str(status)) dialog.format_secondary_text(output) dialog.run() dialog.destroy() box.load_model_list() def on_model_delete(self, button): selection = self.treeview.get_selection() (listmodel, rowlist) = selection.get_selected_rows() if len(rowlist) == 1: id = listmodel.get_value(listmodel.get_iter(rowlist[0]), 0) name = listmodel.get_value(listmodel.get_iter(rowlist[0]), 2) dialog = gtk.MessageDialog(parent=self, flags=gtk.DialogFlags.MODAL, buttons=gtk.ButtonsType.OK_CANCEL) dialog.set_title(_("Confirm Model Deletion")) dialog.props.text = _("Are you sure you want to delete model {id} ({name})?").format(id=id, name=name) response = dialog.run() dialog.destroy() if response == gtk.ResponseType.OK: status, output = subprocess.getstatusoutput(["howdy remove " + id + " -y -U " + self.active_user]) if status != 0: dialog = gtk.MessageDialog(parent=self, flags=gtk.DialogFlags.MODAL, type=gtk.MessageType.ERROR, buttons=gtk.ButtonsType.CLOSE) dialog.set_title(_("Howdy Error")) dialog.props.text = _("Error while deleting model, error code {}: \n\n").format(status) dialog.format_secondary_text(output) dialog.run() dialog.destroy() self.load_model_list() ================================================ FILE: howdy-gtk/src/tab_video.py ================================================ import configparser from i18n import _ import paths_factory from gi.repository import Gtk as gtk from gi.repository import Gdk as gdk from gi.repository import GdkPixbuf as pixbuf from gi.repository import GObject as gobject MAX_HEIGHT = 300 MAX_WIDTH = 300 def on_page_switch(self, notebook, page, page_num): if page_num == 1: try: self.config = configparser.ConfigParser() self.config.read(paths_factory.config_file_path()) except Exception: print(_("Can't open camera")) path = self.config.get("video", "device_path") try: # if not self.cv2: import cv2 self.cv2 = cv2 except Exception: print(_("Can't import OpenCV2")) try: self.capture = cv2.VideoCapture(path) except Exception: print(_("Can't open camera")) opencvbox = self.builder.get_object("opencvbox") opencvbox.modify_bg(gtk.StateType.NORMAL, gdk.Color(red=0, green=0, blue=0)) height = self.capture.get(self.cv2.CAP_PROP_FRAME_HEIGHT) or 1 width = self.capture.get(self.cv2.CAP_PROP_FRAME_WIDTH) or 1 self.scaling_factor = (MAX_HEIGHT / height) or 1 if width * self.scaling_factor > MAX_WIDTH: self.scaling_factor = (MAX_WIDTH / width) or 1 config_height = self.config.getfloat("video", "max_height", fallback=320.0) config_scaling = (config_height / height) or 1 self.builder.get_object("videoid").set_text(path.split("/")[-1]) self.builder.get_object("videores").set_text(str(int(width)) + "x" + str(int(height))) self.builder.get_object("videoresused").set_text(str(int(width * config_scaling)) + "x" + str(int(height * config_scaling))) self.builder.get_object("videorecorder").set_text(self.config.get("video", "recording_plugin", fallback=_("Unknown"))) gobject.timeout_add(10, self.capture_frame) elif self.capture is not None: self.capture.release() self.capture = None def capture_frame(self): if self.capture is None: return ret, frame = self.capture.read() frame = self.cv2.resize(frame, None, fx=self.scaling_factor, fy=self.scaling_factor, interpolation=self.cv2.INTER_AREA) retval, buffer = self.cv2.imencode(".png", frame) loader = pixbuf.PixbufLoader() loader.write(buffer) loader.close() buffer = loader.get_pixbuf() self.opencvimage.set_from_pixbuf(buffer) gobject.timeout_add(20, self.capture_frame) ================================================ FILE: howdy-gtk/src/window.py ================================================ # Opens and controls main ui window import gi import signal import sys import os import elevate import subprocess from i18n import _ import paths_factory # Make sure we have the libs we need gi.require_version("Gtk", "3.0") gi.require_version("Gdk", "3.0") # Import them from gi.repository import Gtk as gtk class MainWindow(gtk.Window): def __init__(self): """Initialize the sticky window""" # Make the class a GTK window gtk.Window.__init__(self) self.builder = gtk.Builder() self.builder.add_from_file(paths_factory.main_window_wireframe_path()) self.builder.connect_signals(self) self.window = self.builder.get_object("mainwindow") self.userlist = self.builder.get_object("userlist") self.modellistbox = self.builder.get_object("modellistbox") self.opencvimage = self.builder.get_object("opencvimage") self.window.connect("destroy", self.exit) self.window.connect("delete_event", self.exit) # Init capture for video tab self.capture = None # Create a treeview that will list the model data self.treeview = gtk.TreeView() self.treeview.set_vexpand(True) # Set the columns for i, column in enumerate([_("ID"), _("Created"), _("Label")]): col = gtk.TreeViewColumn(column, gtk.CellRendererText(), text=i) self.treeview.append_column(col) # Add the treeview self.modellistbox.add(self.treeview) filelist = os.listdir(paths_factory.user_models_dir_path()) self.active_user = "" self.userlist.items = 0 for file in filelist: self.userlist.append_text(file[:-4]) self.userlist.items += 1 if not self.active_user: self.active_user = file[:-4] self.userlist.set_active(0) self.window.show_all() # Start GTK main loop gtk.main() def load_model_list(self): """(Re)load the model list""" # Get username and default to none if there are no models at all yet user = 'none' if self.active_user: user = self.active_user # Execute the list command to get the models status, output = subprocess.getstatusoutput(["howdy list --plain -U " + user]) # Create a datamodel self.listmodel = gtk.ListStore(str, str, str) # If there was no error if status == 0: # Split the output per line lines = output.split("\n") # Add the models to the datamodel for i in range(len(lines)): items = lines[i].split(",") if len(items) < 3: continue self.listmodel.append(items) self.treeview.set_model(self.listmodel) def on_about_link(self, label, uri): """Open links on about page as a non-root user""" try: user = os.getlogin() except Exception: user = os.environ.get("SUDO_USER") status, output = subprocess.getstatusoutput(["sudo -u " + user + " timeout 10 xdg-open " + uri]) return True def exit(self, widget=None, context=None): """Cleanly exit""" if self.capture is not None: self.capture.release() gtk.main_quit() sys.exit(0) # Make sure we quit on a SIGINT signal.signal(signal.SIGINT, signal.SIG_DFL) # Make sure we run as sudo elevate.elevate() # If no models have been created yet or when it is forced, start the onboarding if "--force-onboarding" in sys.argv or not os.path.exists(paths_factory.user_models_dir_path()): import onboarding onboarding.OnboardingWindow() sys.exit(0) # Class is split so it isn't too long, import split functions import tab_models MainWindow.on_user_add = tab_models.on_user_add MainWindow.on_user_change = tab_models.on_user_change MainWindow.on_model_add = tab_models.on_model_add MainWindow.on_model_delete = tab_models.on_model_delete import tab_video MainWindow.on_page_switch = tab_video.on_page_switch MainWindow.capture_frame = tab_video.capture_frame # Open the GTK window window = MainWindow() ================================================ FILE: meson.build ================================================ project('howdy', 'cpp', license: 'MIT', version: 'beta', meson_version: '>= 0.64.0') dlibdatadir = get_option('dlib_data_dir') != '' ? get_option('dlib_data_dir') : join_paths(get_option('prefix'), get_option('datadir'), 'dlib-data') confdir = get_option('config_dir') != '' ? get_option('config_dir') : join_paths(get_option('prefix'), get_option('sysconfdir'), 'howdy') usermodelsdir = get_option('user_models_dir') != '' ? get_option('user_models_dir') : join_paths(confdir, 'models') logpath = get_option('log_path') pythonpath = get_option('python_path') config_path = join_paths(confdir, 'config.ini') paths_dict = { 'config_dir': confdir, 'dlib_data_dir': dlibdatadir, 'user_models_dir': usermodelsdir, 'log_path': logpath, 'python_path': pythonpath } # We need to keep this order beause howdy-gtk defines the gtk script path which is used later in howdy subdir('howdy-gtk') subdir('howdy') ================================================ FILE: meson.options ================================================ option('pam_dir', type: 'string', value: '', description: 'Set the pam_howdy destination directory') #option('fetch_dlib_data', type: 'boolean', value: false, description: 'Download dlib data files') option('config_dir', type: 'string', value: '', description: 'Set the howdy config directory') option('dlib_data_dir', type: 'string', value: '', description: 'Set the dlib data directory') option('user_models_dir', type: 'string', value: '', description: 'Set the user models directory') option('log_path', type: 'string', value: '/var/log/howdy', description: 'Set the log file path') option('install_in_site_packages', type: 'boolean', value: false, description: 'Install howdy python files in site packages') option('py_sources_dir', type: 'string', value: '', description: 'Set the python sources directory') option('install_pam_config', type: 'boolean', value: false, description: 'Install pam config file (for Debian/Ubuntu)') option('python_path', type: 'string', value: '/usr/bin/python', description: 'Set the path to the python executable') option('with_polkit', type: 'boolean', value: false, description: 'Install polkit policy config file')