Showing preview only (299K chars total). Download the full file or copy to clipboard to get everything.
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
================================================

<p align="center">
<a href="https://github.com/boltgolt/howdy/releases">
<img src="https://img.shields.io/github/release/boltgolt/howdy.svg?colorB=4c1">
</a>
<a href="https://github.com/boltgolt/howdy/graphs/contributors">
<img src="https://img.shields.io/github/contributors/boltgolt/howdy.svg?style=flat">
</a>
<a href="https://www.buymeacoffee.com/boltgolt">
<img src="https://img.shields.io/badge/endpoint.svg?url=https://boltgolt.nl/howdy/shield.json">
</a>
<a href="https://actions-badge.atrox.dev/boltgolt/howdy/goto?ref=beta">
<img src="https://img.shields.io/endpoint.svg?url=https%3A%2F%2Factions-badge.atrox.dev%2Fboltgolt%2Fhowdy%2Fbadge%3Fref%3Dbeta&style=flat&label=build&logo=none">
</a>
<a href="https://aur.archlinux.org/packages/howdy">
<img src="https://img.shields.io/aur/votes/howdy?color=4c1&label=aur%20votes">
</a>
</p>
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://github.com/boltgolt/howdy/tree/dev) [](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 <frank@tackitt.net>
# Maintainer: boltgolt <boltgolt@gmail.com>
# Co-Maintainer: Raymo111 <hi@raymond.li>
# Contributor: Kelley McChesney <kelley@kelleymcchesney.us>
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 <boltgolt@gmail.com> 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 <boltgolt@gmail.com> 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 <boltgolt@gmail.com> 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 <boltgolt@gmail.com> 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 <boltgolt@gmail.com> 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 <boltgolt@gmail.com> 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 <boltgolt@gmail.com> 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 <boltgolt@gmail.com> 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 <boltgolt@gmail.com> 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 <boltgolt@gmail.com> Thu, 10 May 2018 15:14:03 +0200
howdy (2.1.0) xenial; urgency=medium
* First complete PPA release
* Reworked CLI
-- boltgolt <boltgolt@gmail.com> 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 <boltgolt@gmail.com> Thu, 12 Apr 2018 21:42:42 +0000
howdy (2.0.0-alpha+2) xenial; urgency=medium
* Fixed build dependency issue
-- boltgolt <boltgolt@gmail.com> Sat, 07 Apr 2018 21:30:48 +0200
howdy (2.0.0-alpha+1) xenial; urgency=low
* Initial packaged release.
-- boltgolt <boltgolt@gmail.com> 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 <boltgolt@gmail.com>
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 <cstring>
#include <memory>
#include <stdexcept>
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 <libevdev/libevdev-uinput.h>
#include <libevdev/libevdev.h>
#include <memory>
class EnterDevice {
std::unique_ptr<struct libevdev, decltype(&libevdev_free)> raw_device;
std::unique_ptr<struct libevdev_uinput, decltype(&libevdev_uinput_destroy)>
raw_uinput_device;
public:
EnterDevice();
void send_enter_press() const;
~EnterDevice() = default;
};
#endif // ENTER_DEVICE_H
================================================
FILE: howdy/src/pam/main.cc
================================================
#include <cerrno>
#include <csignal>
#include <cstdlib>
#include <glob.h>
#include <libintl.h>
#include <pthread.h>
#include <spawn.h>
#include <stdexcept>
#include <sys/signalfd.h>
#include <sys/stat.h>
#include <sys/syslog.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <syslog.h>
#include <unistd.h>
#include <chrono>
#include <condition_variable>
#include <cstring>
#include <fstream>
#include <functional>
#include <future>
#include <mutex>
#include <string>
#include <tuple>
#include <INIReader.h>
#include <security/pam_appl.h>
#include <security/pam_ext.h>
#include <security/pam_modules.h>
#include "enter_device.hh"
#include "main.hh"
#include "optional_task.hh"
#include <paths.hh>
const auto DEFAULT_TIMEOUT =
std::chrono::duration<int, std::chrono::milliseconds::period>(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<int(int, const char *)> &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<int(int, const char *)> &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<char>(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<const char **>(&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<const void **>(reinterpret_cast<void **>(&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<char *const *>(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<int> child_task([&] {
int status;
waitpid(child_pid, &status, 0);
{
std::unique_lock<std::mutex> 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<std::tuple<int, char *>> pass_task([&] {
char *auth_tok_ptr = nullptr;
int pam_res = pam_get_authtok(
pamh, PAM_AUTHTOK, const_cast<const char **>(&auth_tok_ptr), nullptr);
{
std::unique_lock<std::mutex> lock(mutx);
if (confirmation_type == ConfirmationType::Unset) {
confirmation_type = ConfirmationType::Pam;
}
}
convar.notify_one();
return std::tuple<int, char *>(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<std::mutex> 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 <cstring>
#include <string>
#include <unistd.h>
#include <cstdint>
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 <cassert>
#include <chrono>
#include <future>
#include <thread>
// A task executed only if activated.
template <typename T> class optional_task {
std::thread thread;
std::packaged_task<T()> task;
std::future<T> future;
bool spawned{false};
bool is_active{false};
public:
explicit optional_task(std::function<T()> func);
void activate();
template <typename R, typename P>
auto wait(std::chrono::duration<R, P> dur) -> std::future_status;
auto get() -> T;
void stop(bool force);
~optional_task();
};
template <typename T>
optional_task<T>::optional_task(std::function<T()> func)
: task(std::packaged_task<T()>(std::move(func))), future(task.get_future()) {}
// Create a new thread and launch the task on it.
template <typename T> void optional_task<T>::activate() {
thread = std::thread(std::move(task));
spawned = true;
is_active = true;
}
// Wait for `dur` time and return a `future` status.
template <typename T>
template <typename R, typename P>
auto optional_task<T>::wait(std::chrono::duration<R, P> 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 <typename T> auto optional_task<T>::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 <typename T> void optional_task<T>::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 <typename T> optional_task<T>::~optional_task<T>() {
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 <boltgolt@gmail.com> 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 <boltgolt@gmail.com>
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_
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
SYMBOL INDEX (194 symbols across 22 files)
FILE: howdy-gtk/src/authsticky.py
class StickyWindow (line 25) | class StickyWindow(gtk.Window):
method __init__ (line 30) | def __init__(self):
method draw (line 90) | def draw(self, widget, ctx):
method catch_stdin (line 127) | def catch_stdin(self):
method exit (line 148) | def exit(self, widget, context):
FILE: howdy-gtk/src/onboarding.py
class OnboardingWindow (line 16) | class OnboardingWindow(gtk.Window):
method __init__ (line 17) | def __init__(self):
method go_next_slide (line 51) | def go_next_slide(self, button=None):
method execute_slide1 (line 73) | def execute_slide1(self):
method read_download_line (line 89) | def read_download_line(self):
method execute_slide2 (line 112) | def execute_slide2(self):
method execute_slide3 (line 198) | def execute_slide3(self):
method slide3_button_yes (line 224) | def slide3_button_yes(self, button):
method slide3_button_no (line 228) | def slide3_button_no(self, button):
method execute_slide4 (line 234) | def execute_slide4(self):
method on_scanbutton_click (line 246) | def on_scanbutton_click(self, button):
method run_add (line 260) | def run_add(self):
method execute_slide5 (line 273) | def execute_slide5(self):
method execute_slide6 (line 276) | def execute_slide6(self):
method enable_next (line 308) | def enable_next(self):
method show_error (line 312) | def show_error(self, error, secon=""):
method exit (line 323) | def exit(self, widget=None, context=None):
FILE: howdy-gtk/src/paths_factory.py
function config_file_path (line 5) | def config_file_path() -> str:
function user_models_dir_path (line 10) | def user_models_dir_path() -> PurePath:
function logo_path (line 15) | def logo_path() -> str:
function onboarding_wireframe_path (line 20) | def onboarding_wireframe_path() -> str:
function main_window_wireframe_path (line 25) | def main_window_wireframe_path() -> str:
function dlib_data_dir_path (line 30) | def dlib_data_dir_path() -> PurePath:
FILE: howdy-gtk/src/tab_models.py
function on_user_change (line 9) | def on_user_change(self, select):
function on_user_add (line 14) | def on_user_add(self, button):
function on_model_add (line 47) | def on_model_add(self, button):
function execute_add (line 83) | def execute_add(box, dialog, entered_name):
function on_model_delete (line 99) | def on_model_delete(self, button):
FILE: howdy-gtk/src/tab_video.py
function on_page_switch (line 15) | def on_page_switch(self, notebook, page, page_num):
function capture_frame (line 64) | def capture_frame(self):
FILE: howdy-gtk/src/window.py
class MainWindow (line 20) | class MainWindow(gtk.Window):
method __init__ (line 21) | def __init__(self):
method load_model_list (line 72) | def load_model_list(self):
method on_about_link (line 98) | def on_about_link(self, label, uri):
method exit (line 108) | def exit(self, widget=None, context=None):
FILE: howdy/src/cli/test.py
function mouse (line 42) | def mouse(event, x, y, flags, param):
function print_text (line 51) | def print_text(line_number, text):
FILE: howdy/src/compare.py
function exit (line 29) | def exit(code=None):
function init_detector (line 42) | def init_detector(lock):
function make_snapshot (line 69) | def make_snapshot(type):
function send_to_ui (line 81) | def send_to_ui(type, message):
function print_timing (line 330) | def print_timing(label, k):
FILE: howdy/src/pam/enter_device.cc
type libevdev_uinput (line 17) | struct libevdev_uinput
FILE: howdy/src/pam/enter_device.hh
class EnterDevice (line 8) | class EnterDevice {
type libevdev (line 9) | struct libevdev
type libevdev_uinput (line 10) | struct libevdev_uinput
FILE: howdy/src/pam/main.cc
function howdy_error (line 51) | auto howdy_error(int status,
function howdy_status (line 105) | auto howdy_status(char *username, int status, const INIReader &config,
function check_enabled (line 133) | auto check_enabled(const INIReader &config, const char *username) -> int {
function identify (line 198) | auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv,
function pam_sm_authenticate (line 429) | PAM_EXTERN auto pam_sm_authenticate(pam_handle_t *pamh, int flags, int a...
function pam_sm_open_session (line 435) | PAM_EXTERN auto pam_sm_open_session(pam_handle_t *pamh, int flags, int a...
function pam_sm_acct_mgmt (line 441) | PAM_EXTERN auto pam_sm_acct_mgmt(pam_handle_t *pamh, int flags, int argc,
function pam_sm_close_session (line 445) | PAM_EXTERN auto pam_sm_close_session(pam_handle_t *pamh, int flags, int ...
function pam_sm_chauthtok (line 449) | PAM_EXTERN auto pam_sm_chauthtok(pam_handle_t *pamh, int flags, int argc,
function pam_sm_setcred (line 453) | PAM_EXTERN auto pam_sm_setcred(pam_handle_t *pamh, int flags, int argc,
FILE: howdy/src/pam/main.hh
type ConfirmationType (line 9) | enum class ConfirmationType : std::uint8_t { Unset, Howdy, Pam }
type Workaround (line 10) | enum class Workaround : std::uint8_t { Off, Input, Native }
type CompareError (line 13) | enum CompareError : std::uint8_t {
function get_workaround (line 22) | inline auto get_workaround(const std::string &workaround) -> Workaround {
function checkenv (line 43) | auto checkenv(const char *name) -> bool {
FILE: howdy/src/pam/optional_task.hh
class optional_task (line 10) | class optional_task {
FILE: howdy/src/paths_factory.py
function dlib_data_dir_path (line 11) | def dlib_data_dir_path() -> str:
function shape_predictor_5_face_landmarks_path (line 15) | def shape_predictor_5_face_landmarks_path() -> str:
function mmod_human_face_detector_path (line 19) | def mmod_human_face_detector_path() -> str:
function dlib_face_recognition_resnet_model_v1_path (line 23) | def dlib_face_recognition_resnet_model_v1_path() -> str:
function user_model_path (line 27) | def user_model_path(user: str) -> str:
function config_file_path (line 31) | def config_file_path() -> str:
function snapshots_dir_path (line 35) | def snapshots_dir_path() -> PurePath:
function snapshot_path (line 39) | def snapshot_path(snapshot: str) -> str:
function user_models_dir_path (line 43) | def user_models_dir_path() -> PurePath:
function logo_path (line 47) | def logo_path() -> str:
FILE: howdy/src/recorders/ffmpeg_reader.py
class ffmpeg_reader (line 21) | class ffmpeg_reader:
method __init__ (line 24) | def __init__(self, device_path, device_format, numframes=10):
method set (line 34) | def set(self, prop, setting):
method get (line 41) | def get(self, prop):
method probe (line 48) | def probe(self):
method record (line 76) | def record(self, numframes):
method read (line 99) | def read(self):
method release (line 127) | def release(self):
method grab (line 132) | def grab(self):
FILE: howdy/src/recorders/pyv4l2_reader.py
class pyv4l2_reader (line 20) | class pyv4l2_reader:
method __init__ (line 24) | def __init__(self, device_name, device_format):
method set (line 32) | def set(self, prop, setting):
method get (line 39) | def get(self, prop):
method probe (line 46) | def probe(self):
method record (line 70) | def record(self):
method grab (line 74) | def grab(self):
method read (line 78) | def read(self):
method release (line 99) | def release(self):
FILE: howdy/src/recorders/v4l2.py
function _IOC (line 67) | def _IOC(dir_, type_, nr, size):
function _IOC_TYPECHECK (line 75) | def _IOC_TYPECHECK(t):
function _IO (line 79) | def _IO(type_, nr):
function _IOW (line 83) | def _IOW(type_, nr, size):
function _IOR (line 87) | def _IOR(type_, nr, size):
function _IOWR (line 91) | def _IOWR(type_, nr, size):
class timeval (line 107) | class timeval(ctypes.Structure):
function v4l2_fourcc (line 138) | def v4l2_fourcc(a, b, c, d):
function V4L2_FIELD_HAS_TOP (line 157) | def V4L2_FIELD_HAS_TOP(field):
function V4L2_FIELD_HAS_BOTTOM (line 167) | def V4L2_FIELD_HAS_BOTTOM(field):
function V4L2_FIELD_HAS_BOTH (line 177) | def V4L2_FIELD_HAS_BOTH(field):
class v4l2_rect (line 251) | class v4l2_rect(ctypes.Structure):
class v4l2_fract (line 260) | class v4l2_fract(ctypes.Structure):
class v4l2_capability (line 271) | class v4l2_capability(ctypes.Structure):
class v4l2_pix_format (line 312) | class v4l2_pix_format(ctypes.Structure):
class v4l2_fmtdesc (line 414) | class v4l2_fmtdesc(ctypes.Structure):
class v4l2_frmsize_discrete (line 440) | class v4l2_frmsize_discrete(ctypes.Structure):
class v4l2_frmsize_stepwise (line 447) | class v4l2_frmsize_stepwise(ctypes.Structure):
class v4l2_frmsizeenum (line 458) | class v4l2_frmsizeenum(ctypes.Structure):
class _u (line 459) | class _u(ctypes.Union):
class v4l2_frmival_stepwise (line 488) | class v4l2_frmival_stepwise(ctypes.Structure):
class v4l2_frmivalenum (line 496) | class v4l2_frmivalenum(ctypes.Structure):
class _u (line 497) | class _u(ctypes.Union):
class v4l2_timecode (line 520) | class v4l2_timecode(ctypes.Structure):
class v4l2_jpegcompression (line 545) | class v4l2_jpegcompression(ctypes.Structure):
class v4l2_requestbuffers (line 568) | class v4l2_requestbuffers(ctypes.Structure):
class v4l2_buffer (line 577) | class v4l2_buffer(ctypes.Structure):
class _u (line 578) | class _u(ctypes.Union):
class v4l2_framebuffer (line 615) | class v4l2_framebuffer(ctypes.Structure):
class v4l2_clip (line 641) | class v4l2_clip(ctypes.Structure):
class v4l2_window (line 649) | class v4l2_window(ctypes.Structure):
class v4l2_captureparm (line 665) | class v4l2_captureparm(ctypes.Structure):
class v4l2_outputparm (line 680) | class v4l2_outputparm(ctypes.Structure):
class v4l2_cropcap (line 695) | class v4l2_cropcap(ctypes.Structure):
class v4l2_crop (line 704) | class v4l2_crop(ctypes.Structure):
class v4l2_standard (line 772) | class v4l2_standard(ctypes.Structure):
class v4l2_dv_preset (line 787) | class v4l2_dv_preset(ctypes.Structure):
class v4l2_dv_enum_preset (line 798) | class v4l2_dv_enum_preset(ctypes.Structure):
class v4l2_bt_timings (line 837) | class v4l2_bt_timings(ctypes.Structure):
class v4l2_dv_timings (line 867) | class v4l2_dv_timings(ctypes.Structure):
class _u (line 868) | class _u(ctypes.Union):
class v4l2_input (line 891) | class v4l2_input(ctypes.Structure):
class v4l2_output (line 933) | class v4l2_output(ctypes.Structure):
class v4l2_control (line 957) | class v4l2_control(ctypes.Structure):
class v4l2_ext_control (line 964) | class v4l2_ext_control(ctypes.Structure):
class _u (line 965) | class _u(ctypes.Union):
class v4l2_ext_controls (line 982) | class v4l2_ext_controls(ctypes.Structure):
function V4L2_CTRL_ID_MASK (line 998) | def V4L2_CTRL_ID_MASK():
function V4L2_CTRL_ID2CLASS (line 1002) | def V4L2_CTRL_ID2CLASS(id_):
function V4L2_CTRL_DRIVER_PRIV (line 1006) | def V4L2_CTRL_DRIVER_PRIV(id_):
class v4l2_queryctrl (line 1010) | class v4l2_queryctrl(ctypes.Structure):
class v4l2_querymenu (line 1024) | class v4l2_querymenu(ctypes.Structure):
class v4l2_tuner (line 1446) | class v4l2_tuner(ctypes.Structure):
class v4l2_modulator (line 1462) | class v4l2_modulator(ctypes.Structure):
class v4l2_frequency (line 1497) | class v4l2_frequency(ctypes.Structure):
class v4l2_hw_freq_seek (line 1506) | class v4l2_hw_freq_seek(ctypes.Structure):
class v4l2_rds_data (line 1520) | class v4l2_rds_data(ctypes.Structure):
class v4l2_audio (line 1546) | class v4l2_audio(ctypes.Structure):
class v4l2_audioout (line 1562) | class v4l2_audioout(ctypes.Structure):
class v4l2_enc_idx_entry (line 1582) | class v4l2_enc_idx_entry(ctypes.Structure):
class v4l2_enc_idx (line 1595) | class v4l2_enc_idx(ctypes.Structure):
class v4l2_encoder_cmd (line 1612) | class v4l2_encoder_cmd(ctypes.Structure):
class _u (line 1613) | class _u(ctypes.Union):
class _s (line 1614) | class _s(ctypes.Structure):
class v4l2_vbi_format (line 1636) | class v4l2_vbi_format(ctypes.Structure):
class v4l2_sliced_vbi_format (line 1653) | class v4l2_sliced_vbi_format(ctypes.Structure):
class v4l2_sliced_vbi_cap (line 1671) | class v4l2_sliced_vbi_cap(ctypes.Structure):
class v4l2_sliced_vbi_data (line 1680) | class v4l2_sliced_vbi_data(ctypes.Structure):
class v4l2_mpeg_vbi_itv0_line (line 1701) | class v4l2_mpeg_vbi_itv0_line(ctypes.Structure):
class v4l2_mpeg_vbi_itv0 (line 1710) | class v4l2_mpeg_vbi_itv0(ctypes.Structure):
class v4l2_mpeg_vbi_ITV0 (line 1719) | class v4l2_mpeg_vbi_ITV0(ctypes.Structure):
class v4l2_mpeg_vbi_fmt_ivtv (line 1731) | class v4l2_mpeg_vbi_fmt_ivtv(ctypes.Structure):
class _u (line 1732) | class _u(ctypes.Union):
class v4l2_format (line 1751) | class v4l2_format(ctypes.Structure):
class _u (line 1752) | class _u(ctypes.Union):
class v4l2_streamparm (line 1767) | class v4l2_streamparm(ctypes.Structure):
class _u (line 1768) | class _u(ctypes.Union):
class v4l2_dbg_match (line 1791) | class v4l2_dbg_match(ctypes.Structure):
class _u (line 1792) | class _u(ctypes.Union):
class v4l2_dbg_register (line 1807) | class v4l2_dbg_register(ctypes.Structure):
class v4l2_dbg_chip_ident (line 1818) | class v4l2_dbg_chip_ident(ctypes.Structure):
FILE: howdy/src/recorders/video_capture.py
class VideoCapture (line 18) | class VideoCapture:
method __init__ (line 19) | def __init__(self, config):
method __del__ (line 54) | def __del__(self):
method release (line 64) | def release(self):
method read_frame (line 71) | def read_frame(self):
method _create_reader (line 99) | def _create_reader(self):
FILE: howdy/src/rubberstamps/__init__.py
class RubberStamp (line 10) | class RubberStamp:
method set_ui_text (line 16) | def set_ui_text(self, text, type=None):
method send_ui_raw (line 25) | def send_ui_raw(self, command):
function execute (line 44) | def execute(config, gtk_proc, opencv):
FILE: howdy/src/rubberstamps/hotkey.py
class hotkey (line 10) | class hotkey(RubberStamp):
method declare_config (line 13) | def declare_config(self):
method run (line 18) | def run(self):
method on_key (line 67) | def on_key(self, type):
FILE: howdy/src/rubberstamps/nod.py
class nod (line 9) | class nod(RubberStamp):
method declare_config (line 10) | def declare_config(self):
method run (line 15) | def run(self):
FILE: howdy/src/snapshot.py
function generate (line 11) | def generate(frames, text_lines):
Condensed preview — 98 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (305K chars).
[
{
"path": ".clang-tidy",
"chars": 638,
"preview": "Checks: 'clang-diagnostic-*,clang-analyser-*,-clang-diagnostic-unused-command-line-argument,google-*,bugprone-*,moderniz"
},
{
"path": ".github/FUNDING.yml",
"chars": 92,
"preview": "# These are supported funding model platforms\ncustom: https://www.buymeacoffee.com/boltgolt\n"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 440,
"preview": "---\nname: Bug report\nabout: Report something that's not working\n\n---\n\n_Please describe the issue in as much detail as po"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.md",
"chars": 76,
"preview": "---\nname: Feature request\nabout: 'Suggest a feature or improvement '\n\n---\n\n\n"
},
{
"path": ".github/pull_request_template.md",
"chars": 101,
"preview": "_Please make sure to target the \"dev\" branch if it exists_\n_REMOVE THIS MESSAGE IN THE PULL REQUEST_\n"
},
{
"path": ".github/workflows/check.yml",
"chars": 698,
"preview": "name: check\non: [push, pull_request]\n\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - name: Install required"
},
{
"path": ".gitignore",
"chars": 1574,
"preview": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packagi"
},
{
"path": "LICENSE",
"chars": 1065,
"preview": "MIT License\n\nCopyright (c) 2018 boltgolt\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\no"
},
{
"path": "README.md",
"chars": 7280,
"preview": "\n\n<p align=\"center\">\n\t<a href=\"https://github.com/boltgolt/howdy/releases\">\n\t\t<"
},
{
"path": "howdy/archlinux/.gitignore",
"chars": 49,
"preview": "pkg\nsrc\n*.tar.gz\n*.zip\n*.tar.xz\n*.patch\n*.dat.bz2"
},
{
"path": "howdy/archlinux/howdy/.gitignore",
"chars": 59,
"preview": "pkg\nsrc\n*.tar.gz\n*.zip\n*.tar.xz\n*.patch\n*.dat.bz2\n.SRCINFO\n"
},
{
"path": "howdy/archlinux/howdy/PKGBUILD",
"chars": 2064,
"preview": "# Maintainer: Frank Tackitt <frank@tackitt.net>\n# Maintainer: boltgolt <boltgolt@gmail.com>\n# Co-Maintainer: Raymo111 <h"
},
{
"path": "howdy/debian/changelog",
"chars": 6144,
"preview": "howdy (3.0.0) focal; urgency=medium\n\n * Way too many changes to all list individually, thanks to everyone who contribut"
},
{
"path": "howdy/debian/compat",
"chars": 3,
"preview": "10\n"
},
{
"path": "howdy/debian/control",
"chars": 950,
"preview": "Source: howdy\nSection: misc\nPriority: optional\nStandards-Version: 3.9.7\nBuild-Depends: devscripts, git, dh-make, debhelp"
},
{
"path": "howdy/debian/copyright",
"chars": 1065,
"preview": "MIT License\n\nCopyright (c) 2018 boltgolt\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\no"
},
{
"path": "howdy/debian/howdy.lintian-overrides",
"chars": 269,
"preview": "# W: Don't require ugly linebreaks in last 5 chars\nhowdy: debian-changelog-line-too-long\n\n# E: Allows the name Howdy to "
},
{
"path": "howdy/debian/howdy.manpages",
"chars": 8,
"preview": "howdy.1\n"
},
{
"path": "howdy/debian/install",
"chars": 532,
"preview": "src/cli/. lib/security/howdy/cli\nsrc/locales/. lib/security/howdy/locales\nsrc/recorders/. lib/security/howdy/recorders\ns"
},
{
"path": "howdy/debian/postinst",
"chars": 5951,
"preview": "#!/usr/bin/python3\n# Installation script to install howdy\n# Executed after primary apt install\n\n# Import required module"
},
{
"path": "howdy/debian/preinst",
"chars": 1086,
"preview": "#!/usr/bin/python3\n# Used to check cameras before committing to install\n# Executed before primary apt install of files\n\n"
},
{
"path": "howdy/debian/prerm",
"chars": 1163,
"preview": "#!/usr/bin/python3\n# Executed on deinstallation\n# Completely remove howdy from the system\n\n# Import required modules\nimp"
},
{
"path": "howdy/debian/rules",
"chars": 446,
"preview": "#!/usr/bin/make -f\nDH_VERBOSE = 1\n\nDPKG_EXPORT_BUILDFLAGS = 1\ninclude /usr/share/dpkg/default.mk\n\n%:\n\tdh $@\n\nbuild:\n\t# C"
},
{
"path": "howdy/debian/source/format",
"chars": 13,
"preview": "3.0 (native)\n"
},
{
"path": "howdy/debian/source/options",
"chars": 329,
"preview": "tar-ignore = \".git\"\ntar-ignore = \".gitignore\"\ntar-ignore = \".github\"\ntar-ignore = \"models\"\ntar-ignore = \"snapshots\"\ntar-"
},
{
"path": "howdy/howdy.1",
"chars": 1030,
"preview": ".\\\" Please adjust this date whenever revising the manpage.\n.TH HOWDY 1 \"April 9, 2018\" \"Howdy help\" \"User Commands\"\n.SH "
},
{
"path": "howdy/meson.build",
"chars": 13,
"preview": "subdir('src')"
},
{
"path": "howdy/src/autocomplete/howdy.in",
"chars": 1107,
"preview": "#!/bin/bash\n# Autocomplete file run in bash\n# Will sugest arguments on tab\n\n_howdy() {\n\tlocal cur prev opts\n\tlocal confi"
},
{
"path": "howdy/src/bin/howdy.in",
"chars": 45,
"preview": "#!/bin/sh\n\n@python_path@ \"@script_path@\" \"$@\""
},
{
"path": "howdy/src/cli/__init__.py",
"chars": 34,
"preview": "# Marks this folder as importable\n"
},
{
"path": "howdy/src/cli/add.py",
"chars": 6294,
"preview": "# Save the face of the user in encoded form\n\n# Import required modules\nimport time\nimport os\nimport sys\nimport json\nimpo"
},
{
"path": "howdy/src/cli/clear.py",
"chars": 1015,
"preview": "# Clear all models by deleting the whole file\n\n# Import required modules\nimport os\nimport sys\nimport builtins\nimport pat"
},
{
"path": "howdy/src/cli/config.py",
"chars": 1209,
"preview": "# Open the config file in an editor\n\n# Import required modules\nimport os\nimport subprocess\nimport shutil\nimport paths_fa"
},
{
"path": "howdy/src/cli/disable.py",
"chars": 1481,
"preview": "# Set the disable flag\n\n# Import required modules\nimport sys\nimport os\nimport builtins\nimport fileinput\nimport configpar"
},
{
"path": "howdy/src/cli/list.py",
"chars": 1665,
"preview": "# List all models for a user\n\n# Import required modules\nimport sys\nimport os\nimport json\nimport time\nimport builtins\nimp"
},
{
"path": "howdy/src/cli/remove.py",
"chars": 2521,
"preview": "# Remove a encoding from the models file\n\n# Import required modules\nimport sys\nimport os\nimport json\nimport builtins\nimp"
},
{
"path": "howdy/src/cli/set.py",
"chars": 1271,
"preview": "# Set a config value\n\n# Import required modules\nimport sys\nimport os\nimport builtins\nimport fileinput\nimport paths_facto"
},
{
"path": "howdy/src/cli/snap.py",
"chars": 1364,
"preview": "# Create a snapshot\n\n# Import required modules\nimport os\nimport configparser\nfrom datetime import timezone, datetime\nimp"
},
{
"path": "howdy/src/cli/test.py",
"chars": 7695,
"preview": "# Show a window with the video stream and testing information\n\n# Import required modules\nimport configparser\nimport buil"
},
{
"path": "howdy/src/cli.py",
"chars": 3462,
"preview": "# CLI directly called by running the howdy command\n\n# Import required modules\nimport sys\nimport os\nimport pwd\nimport get"
},
{
"path": "howdy/src/compare.py",
"chars": 12381,
"preview": "# Compare incoming video with known faces\n# Running in a local python instance to get around PATH issues\n\n# Import time "
},
{
"path": "howdy/src/config.ini",
"chars": 4006,
"preview": "# Howdy config file\n# Press CTRL + X to save in the nano editor\n\n[core]\n# Print that face detection is being attempted\nd"
},
{
"path": "howdy/src/dlib-data/.gitignore",
"chars": 16,
"preview": "*.dat\n*.dat.bz2\n"
},
{
"path": "howdy/src/dlib-data/Readme.md",
"chars": 414,
"preview": "Download and unpack `dlib` data files from https://github.com/davisking/dlib-models repository:\n\n```\nshell\nwget https://"
},
{
"path": "howdy/src/dlib-data/install.sh",
"chars": 1322,
"preview": "#!/bin/bash\n\necho \"Downloading 3 required data files...\"\n\n# Check if wget is installed\nif hash wget;then\n\t# Check if wge"
},
{
"path": "howdy/src/i18n.py",
"chars": 357,
"preview": "# Support file for translations\n\n# Import modules\nimport gettext\nimport os\n\n# Get the right translation based on locale,"
},
{
"path": "howdy/src/meson.build",
"chars": 4851,
"preview": "if meson.is_subproject()\nproject('howdy', 'cpp', license: 'MIT', version: 'beta', meson_version: '>= 0.64.0')\nendif\n\n\nda"
},
{
"path": "howdy/src/pam/.gitignore",
"chars": 18,
"preview": "subprojects/inih/\n"
},
{
"path": "howdy/src/pam/README.md",
"chars": 659,
"preview": "# Howdy PAM module\n\n## Requirements\n\nThis module depends on `INIReader` and `libevdev`.\nThey can be installed with these"
},
{
"path": "howdy/src/pam/enter_device.cc",
"chars": 1400,
"preview": "#include \"enter_device.hh\"\n\n#include <cstring>\n#include <memory>\n#include <stdexcept>\n\nEnterDevice::EnterDevice()\n : "
},
{
"path": "howdy/src/pam/enter_device.hh",
"chars": 449,
"preview": "#ifndef ENTER_DEVICE_H_\n#define ENTER_DEVICE_H_\n\n#include <libevdev/libevdev-uinput.h>\n#include <libevdev/libevdev.h>\n#i"
},
{
"path": "howdy/src/pam/main.cc",
"chars": 14938,
"preview": "#include <cerrno>\n#include <csignal>\n#include <cstdlib>\n\n#include <glob.h>\n#include <libintl.h>\n#include <pthread.h>\n#in"
},
{
"path": "howdy/src/pam/main.hh",
"chars": 1346,
"preview": "#ifndef MAIN_H_\n#define MAIN_H_\n\n#include <cstring>\n#include <string>\n#include <unistd.h>\n#include <cstdint>\n\nenum class"
},
{
"path": "howdy/src/pam/meson.build",
"chars": 717,
"preview": "inih_cpp = dependency('INIReader', fallback: ['inih', 'INIReader_dep'])\nlibevdev = dependency('libevdev')\nlibpam = meson"
},
{
"path": "howdy/src/pam/optional_task.hh",
"chars": 2116,
"preview": "#ifndef OPTIONAL_TASK_H_\n#define OPTIONAL_TASK_H_\n\n#include <cassert>\n#include <chrono>\n#include <future>\n#include <thre"
},
{
"path": "howdy/src/pam/paths.hh.in",
"chars": 213,
"preview": "const auto COMPARE_PROCESS_PATH = \"@compare_script_path@\";\nconst auto CONFIG_FILE_PATH = \"@config_file_path@\";\nconst aut"
},
{
"path": "howdy/src/pam/po/LINGUAS",
"chars": 0,
"preview": ""
},
{
"path": "howdy/src/pam/po/POTFILES",
"chars": 7,
"preview": "main.cc"
},
{
"path": "howdy/src/pam/po/meson.build",
"chars": 401,
"preview": "i18n = import('i18n')\n\n# define GETTEXT_PACKAGE and LOCALEDIR\ngettext_package = '-DGETTEXT_PACKAGE=\"@0@\"'.format(meson.p"
},
{
"path": "howdy/src/pam-config/howdy.in",
"chars": 123,
"preview": "Name: Howdy\nDefault: yes\nPriority: 512\nAuth-Type: Primary\nAuth:\n\t[success=end default=ignore] @pamdir@/pam_howdy."
},
{
"path": "howdy/src/paths.py.in",
"chars": 492,
"preview": "from pathlib import PurePath\n\n# Define the absolute path to the config directory\nconfig_dir = PurePath(\"@config_dir@\")\n\n"
},
{
"path": "howdy/src/paths_factory.py",
"chars": 1051,
"preview": "from pathlib import PurePath\nimport paths\n\nmodels = [\n \"shape_predictor_5_face_landmarks.dat\",\n \"mmod_human_face_d"
},
{
"path": "howdy/src/recorders/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "howdy/src/recorders/ffmpeg_reader.py",
"chars": 4724,
"preview": "# Class that simulates the functionality of opencv so howdy can use ffmpeg seamlessly\n\n# Import required modules\nimport "
},
{
"path": "howdy/src/recorders/pyv4l2_reader.py",
"chars": 2925,
"preview": "# Class that simulates the functionality of opencv so howdy can use v4l2 devices seamlessly\n\n# Import required modules. "
},
{
"path": "howdy/src/recorders/v4l2.py",
"chars": 52253,
"preview": "# Python bindings for the v4l2 userspace api\n\n# Copyright (C) 1999-2009 the contributors\n\n# This program is free softwar"
},
{
"path": "howdy/src/recorders/video_capture.py",
"chars": 4331,
"preview": "# Top level class for a video capture providing simplified API's for common\n# functions\n\n# Import required modules\nimpor"
},
{
"path": "howdy/src/rubberstamps/__init__.py",
"chars": 5083,
"preview": "import sys\nimport os\nimport re\n\nfrom i18n import _\n\nfrom importlib.machinery import SourceFileLoader\n\n\nclass RubberStamp"
},
{
"path": "howdy/src/rubberstamps/hotkey.py",
"chars": 2266,
"preview": "import time\nimport sys\n\nfrom i18n import _\n\n# Import the root rubberstamp class\nfrom rubberstamps import RubberStamp\n\n\nc"
},
{
"path": "howdy/src/rubberstamps/nod.py",
"chars": 3538,
"preview": "import time\n\nfrom i18n import _\n\n# Import the root rubberstamp class\nfrom rubberstamps import RubberStamp\n\n\nclass nod(Ru"
},
{
"path": "howdy/src/snapshot.py",
"chars": 1817,
"preview": "# Create and save snapshots of auth attempts\n\n# Import modules\nimport cv2\nimport os\nfrom datetime import timezone, datet"
},
{
"path": "howdy-gtk/bin/howdy-gtk.in",
"chars": 45,
"preview": "#!/bin/sh\n\n@python_path@ \"@script_path@\" \"$@\""
},
{
"path": "howdy-gtk/debian/changelog",
"chars": 168,
"preview": "howdy-gtk (0.0.1) xenial; urgency=medium\n\n * Initial testing release with sticky authentication ui\n\n -- boltgolt <boltg"
},
{
"path": "howdy-gtk/debian/compat",
"chars": 3,
"preview": "10\n"
},
{
"path": "howdy-gtk/debian/control",
"chars": 485,
"preview": "Source: howdy-gtk\nSection: misc\nPriority: optional\nStandards-Version: 3.9.7\nBuild-Depends: python, dh-python, devscripts"
},
{
"path": "howdy-gtk/debian/copyright",
"chars": 1065,
"preview": "MIT License\n\nCopyright (c) 2020 boltgolt\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\no"
},
{
"path": "howdy-gtk/debian/howdy-gtk.links",
"chars": 46,
"preview": "/usr/lib/howdy-gtk/init.py /usr/bin/howdy-gtk\n"
},
{
"path": "howdy-gtk/debian/howdy-gtk.lintian-overrides",
"chars": 89,
"preview": "# W: Don't require ugly linebreaks in last 5 chars\nhowdy: debian-changelog-line-too-long\n"
},
{
"path": "howdy-gtk/debian/install",
"chars": 25,
"preview": "src/. /usr/lib/howdy-gtk\n"
},
{
"path": "howdy-gtk/debian/postinst",
"chars": 31,
"preview": "#!/bin/sh\npip3 install elevate\n"
},
{
"path": "howdy-gtk/debian/rules",
"chars": 108,
"preview": "#!/usr/bin/make -f\nDH_VERBOSE = 1\n\nDPKG_EXPORT_BUILDFLAGS = 1\ninclude /usr/share/dpkg/default.mk\n\n%:\n\tdh $@\n"
},
{
"path": "howdy-gtk/debian/source/format",
"chars": 13,
"preview": "3.0 (native)\n"
},
{
"path": "howdy-gtk/debian/source/options",
"chars": 192,
"preview": "tar-ignore = \".git\"\ntar-ignore = \".gitignore\"\ntar-ignore = \".github\"\ntar-ignore = \"README.md\"\ntar-ignore = \".travis.yml\""
},
{
"path": "howdy-gtk/meson.build",
"chars": 2459,
"preview": "if meson.is_subproject()\nproject('howdy-gtk', license: 'MIT', version: 'beta', meson_version: '>= 0.64.0')\nendif\n\ndatadi"
},
{
"path": "howdy-gtk/src/authsticky.py",
"chars": 4340,
"preview": "# Shows a floating window when authenticating\nimport cairo\nimport gi\nimport signal\nimport sys\nimport paths_factory\nimpor"
},
{
"path": "howdy-gtk/src/i18n.py",
"chars": 356,
"preview": "# Support file for translations\n\n# Import modules\nimport gettext\nimport os\n\n# Get the right translation based on locale,"
},
{
"path": "howdy-gtk/src/init.py",
"chars": 143,
"preview": "# Opens auth ui if requested, otherwise starts normal ui\nimport sys\n\nif \"--start-auth-ui\" in sys.argv:\n\timport authstick"
},
{
"path": "howdy-gtk/src/main.glade",
"chars": 23886,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!-- Generated with glade 3.18.3 -->\n<interface domain=\"howdy\">\n <requires lib=\""
},
{
"path": "howdy-gtk/src/onboarding.glade",
"chars": 38326,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!-- Generated with glade 3.38.2 -->\n<interface>\n <requires lib=\"gtk+\" version=\""
},
{
"path": "howdy-gtk/src/onboarding.py",
"chars": 10181,
"preview": "import sys\nimport os\nimport re\nimport time\nimport subprocess\nimport paths_factory\n\nfrom i18n import _\n\nfrom gi.repositor"
},
{
"path": "howdy-gtk/src/paths.py.in",
"chars": 425,
"preview": "from pathlib import PurePath\n\n# Define the absolute path to the config directory\nconfig_dir = PurePath(\"@config_dir@\")\n\n"
},
{
"path": "howdy-gtk/src/paths_factory.py",
"chars": 837,
"preview": "from pathlib import PurePath\nimport paths\n\n\ndef config_file_path() -> str:\n \"\"\"Return the path to the config file\"\"\"\n"
},
{
"path": "howdy-gtk/src/polkit/com.github.boltgolt.howdy-gtk.policy.in",
"chars": 919,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE policyconfig PUBLIC \"-//freedesktop//DTD polkit Policy Configuration 1."
},
{
"path": "howdy-gtk/src/tab_models.py",
"chars": 3987,
"preview": "import subprocess\nimport time\n\nfrom i18n import _\nfrom gi.repository import Gtk as gtk\nfrom gi.repository import GObject"
},
{
"path": "howdy-gtk/src/tab_video.py",
"chars": 2294,
"preview": "import configparser\n\nfrom i18n import _\nimport paths_factory\n\nfrom gi.repository import Gtk as gtk\nfrom gi.repository im"
},
{
"path": "howdy-gtk/src/window.py",
"chars": 3703,
"preview": "# Opens and controls main ui window\nimport gi\nimport signal\nimport sys\nimport os\nimport elevate\nimport subprocess\n\nfrom "
},
{
"path": "meson.build",
"chars": 921,
"preview": "project('howdy', 'cpp', license: 'MIT', version: 'beta', meson_version: '>= 0.64.0')\n\ndlibdatadir = get_option('dlib_dat"
},
{
"path": "meson.options",
"chars": 1155,
"preview": "option('pam_dir', type: 'string', value: '', description: 'Set the pam_howdy destination directory')\n#option('fetch_dlib"
}
]
About this extraction
This page contains the full source code of the boltgolt/howdy GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 98 files (276.1 KB), approximately 79.7k tokens, and a symbol index with 194 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.