├── .gitignore ├── Makefile ├── README.md ├── poetry.lock ├── project ├── __init__.py ├── capturers │ ├── __init__.py │ ├── dump │ │ ├── man.png │ │ └── woman.png │ └── haar_blob.py ├── frame_sources │ ├── __init__.py │ ├── camera.py │ ├── file.py │ ├── folder.py │ └── video.py ├── gui │ ├── __init__.py │ ├── application_window.py │ └── assets │ │ ├── GUImain.ui │ │ └── style.css ├── main.py └── settings.py └── pyproject.toml /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | 140 | .idea/ 141 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | linter: 2 | PYTHONPATH=$(shell pwd)/project poetry run black --line-length 120 project 3 | PYTHONPATH=$(shell pwd)/project poetry run isort project -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Eye-Tracker 2 | Modular, Extensible Eye-Tracking solution 3 | [Youtube Video Demonstration](https://youtu.be/zDN-wwd5cfo "Eye tracking") 4 | 5 | ## Overview 6 | A **very** accurate eye-tracking software. 7 | ![What it looks like](https://i.imgur.com/DQRmibk.png) 8 | 9 | ## Features 10 | - Cross-platform 11 | - Works with glasses 12 | - Does not require high-end hardware, works well even with a 640*480 webcam 13 | - Uses blob detection algorithm, but earlier versions used circle detection too. 14 | - Highly extensible/flexible 15 | - New image sources or capture sources can easily be added 16 | 17 | ## Requirements 18 | - Python 3.7 + 19 | 20 | ## Guide 21 | Full installation & run: 22 | 23 | MACOS & linux 24 | ``` 25 | $ python3 -m venv venv 26 | $ source venv/bin/activate 27 | $ pip install poetry 28 | $ poetry install 29 | $ cd project 30 | $ python main.py 31 | ``` 32 | WINDOWS: 33 | 34 | ``` 35 | $ python3 -m venv venv 36 | $ venv\Scripts\activate.bat 37 | $ pip install poetry 38 | $ poetry install 39 | $ cd project 40 | $ python main.py 41 | ``` 42 | 43 | Options & Arguments: 44 | 45 | * `--frame-source` allows you to specify the source of your frames. Currently available options are: `camera`, `folder`, `file`. Defaults to `camera` 46 | 47 | `camera` means images will come from your device's camera 48 | 49 | `folder` means images will come one-by-one from your `DEBUG_DUMP_LOCATION` setting folder in `settings.py` with the interval of `REFRESH_PERIOD` 50 | 51 | `file` means the image will be static, good for debugging or development. Path can be specified in the `STATIC_FILE_PATH` setting 52 | 53 | * There are some Environment variables that can be specified to change the behaviour. Like `DEBUG_DUMP` to set whether a dump should be made when the program crashes 54 | ## Developer 55 | - Stepan Filonov (@stepacool) stepanfilonov@gmail.com 56 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "black" 3 | version = "21.9b0" 4 | description = "The uncompromising code formatter." 5 | category = "dev" 6 | optional = false 7 | python-versions = ">=3.6.2" 8 | 9 | [package.dependencies] 10 | click = ">=7.1.2" 11 | mypy-extensions = ">=0.4.3" 12 | pathspec = ">=0.9.0,<1" 13 | platformdirs = ">=2" 14 | regex = ">=2020.1.8" 15 | tomli = ">=0.2.6,<2.0.0" 16 | typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\""} 17 | typing-extensions = [ 18 | {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}, 19 | {version = "!=3.10.0.1", markers = "python_version >= \"3.10\""}, 20 | ] 21 | 22 | [package.extras] 23 | colorama = ["colorama (>=0.4.3)"] 24 | d = ["aiohttp (>=3.6.0)", "aiohttp-cors (>=0.4.0)"] 25 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 26 | python2 = ["typed-ast (>=1.4.2)"] 27 | uvloop = ["uvloop (>=0.15.2)"] 28 | 29 | [[package]] 30 | name = "click" 31 | version = "8.0.3" 32 | description = "Composable command line interface toolkit" 33 | category = "dev" 34 | optional = false 35 | python-versions = ">=3.6" 36 | 37 | [package.dependencies] 38 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 39 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 40 | 41 | [[package]] 42 | name = "colorama" 43 | version = "0.4.4" 44 | description = "Cross-platform colored terminal text." 45 | category = "dev" 46 | optional = false 47 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 48 | 49 | [[package]] 50 | name = "importlib-metadata" 51 | version = "4.8.1" 52 | description = "Read metadata from Python packages" 53 | category = "dev" 54 | optional = false 55 | python-versions = ">=3.6" 56 | 57 | [package.dependencies] 58 | typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} 59 | zipp = ">=0.5" 60 | 61 | [package.extras] 62 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 63 | perf = ["ipython"] 64 | testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] 65 | 66 | [[package]] 67 | name = "isort" 68 | version = "5.9.3" 69 | description = "A Python utility / library to sort Python imports." 70 | category = "dev" 71 | optional = false 72 | python-versions = ">=3.6.1,<4.0" 73 | 74 | [package.extras] 75 | pipfile_deprecated_finder = ["pipreqs", "requirementslib"] 76 | requirements_deprecated_finder = ["pipreqs", "pip-api"] 77 | colors = ["colorama (>=0.4.3,<0.5.0)"] 78 | plugins = ["setuptools"] 79 | 80 | [[package]] 81 | name = "mypy-extensions" 82 | version = "0.4.3" 83 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 84 | category = "dev" 85 | optional = false 86 | python-versions = "*" 87 | 88 | [[package]] 89 | name = "numpy" 90 | version = "1.21.3" 91 | description = "NumPy is the fundamental package for array computing with Python." 92 | category = "main" 93 | optional = false 94 | python-versions = ">=3.7,<3.11" 95 | 96 | [[package]] 97 | name = "opencv-python" 98 | version = "4.5.4.58" 99 | description = "Wrapper package for OpenCV python bindings." 100 | category = "main" 101 | optional = false 102 | python-versions = ">=3.6" 103 | 104 | [package.dependencies] 105 | numpy = ">=1.21.2" 106 | 107 | [[package]] 108 | name = "pathspec" 109 | version = "0.9.0" 110 | description = "Utility library for gitignore style pattern matching of file paths." 111 | category = "dev" 112 | optional = false 113 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 114 | 115 | [[package]] 116 | name = "platformdirs" 117 | version = "2.4.0" 118 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 119 | category = "dev" 120 | optional = false 121 | python-versions = ">=3.6" 122 | 123 | [package.extras] 124 | docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] 125 | test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] 126 | 127 | [[package]] 128 | name = "pyqt6" 129 | version = "6.2.1" 130 | description = "Python bindings for the Qt cross platform application toolkit" 131 | category = "main" 132 | optional = false 133 | python-versions = ">=3.6.1" 134 | 135 | [package.dependencies] 136 | PyQt6-Qt6 = ">=6.2.1" 137 | PyQt6-sip = ">=13.1,<14" 138 | 139 | [[package]] 140 | name = "pyqt6-qt6" 141 | version = "6.2.1" 142 | description = "The subset of a Qt installation needed by PyQt6." 143 | category = "main" 144 | optional = false 145 | python-versions = "*" 146 | 147 | [[package]] 148 | name = "pyqt6-sip" 149 | version = "13.1.0" 150 | description = "The sip module support for PyQt6" 151 | category = "main" 152 | optional = false 153 | python-versions = ">=3.6" 154 | 155 | [[package]] 156 | name = "regex" 157 | version = "2021.10.23" 158 | description = "Alternative regular expression module, to replace re." 159 | category = "dev" 160 | optional = false 161 | python-versions = "*" 162 | 163 | [[package]] 164 | name = "tomli" 165 | version = "1.2.2" 166 | description = "A lil' TOML parser" 167 | category = "dev" 168 | optional = false 169 | python-versions = ">=3.6" 170 | 171 | [[package]] 172 | name = "typed-ast" 173 | version = "1.4.3" 174 | description = "a fork of Python 2 and 3 ast modules with type comment support" 175 | category = "dev" 176 | optional = false 177 | python-versions = "*" 178 | 179 | [[package]] 180 | name = "typing-extensions" 181 | version = "3.10.0.2" 182 | description = "Backported and Experimental Type Hints for Python 3.5+" 183 | category = "dev" 184 | optional = false 185 | python-versions = "*" 186 | 187 | [[package]] 188 | name = "zipp" 189 | version = "3.6.0" 190 | description = "Backport of pathlib-compatible object wrapper for zip files" 191 | category = "dev" 192 | optional = false 193 | python-versions = ">=3.6" 194 | 195 | [package.extras] 196 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 197 | testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] 198 | 199 | [metadata] 200 | lock-version = "1.1" 201 | python-versions = ">=3.7,<3.11" 202 | content-hash = "43c6a9fc052bbc2a3819d01f2abe5fcc39650affe23330c409b512f9083cdf8f" 203 | 204 | [metadata.files] 205 | black = [ 206 | {file = "black-21.9b0-py3-none-any.whl", hash = "sha256:380f1b5da05e5a1429225676655dddb96f5ae8c75bdf91e53d798871b902a115"}, 207 | {file = "black-21.9b0.tar.gz", hash = "sha256:7de4cfc7eb6b710de325712d40125689101d21d25283eed7e9998722cf10eb91"}, 208 | ] 209 | click = [ 210 | {file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"}, 211 | {file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"}, 212 | ] 213 | colorama = [ 214 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 215 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 216 | ] 217 | importlib-metadata = [ 218 | {file = "importlib_metadata-4.8.1-py3-none-any.whl", hash = "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15"}, 219 | {file = "importlib_metadata-4.8.1.tar.gz", hash = "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"}, 220 | ] 221 | isort = [ 222 | {file = "isort-5.9.3-py3-none-any.whl", hash = "sha256:e17d6e2b81095c9db0a03a8025a957f334d6ea30b26f9ec70805411e5c7c81f2"}, 223 | {file = "isort-5.9.3.tar.gz", hash = "sha256:9c2ea1e62d871267b78307fe511c0838ba0da28698c5732d54e2790bf3ba9899"}, 224 | ] 225 | mypy-extensions = [ 226 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 227 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 228 | ] 229 | numpy = [ 230 | {file = "numpy-1.21.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:508b0b513fa1266875524ba8a9ecc27b02ad771fe1704a16314dc1a816a68737"}, 231 | {file = "numpy-1.21.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5dfe9d6a4c39b8b6edd7990091fea4f852888e41919d0e6722fe78dd421db0eb"}, 232 | {file = "numpy-1.21.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a10968963640e75cc0193e1847616ab4c718e83b6938ae74dea44953950f6b7"}, 233 | {file = "numpy-1.21.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49c6249260890e05b8111ebfc391ed58b3cb4b33e63197b2ec7f776e45330721"}, 234 | {file = "numpy-1.21.3-cp310-cp310-win_amd64.whl", hash = "sha256:f8f4625536926a155b80ad2bbff44f8cc59e9f2ad14cdda7acf4c135b4dc8ff2"}, 235 | {file = "numpy-1.21.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e54af82d68ef8255535a6cdb353f55d6b8cf418a83e2be3569243787a4f4866f"}, 236 | {file = "numpy-1.21.3-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f41b018f126aac18583956c54544db437f25c7ee4794bcb23eb38bef8e5e192a"}, 237 | {file = "numpy-1.21.3-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:50cd26b0cf6664cb3b3dd161ba0a09c9c1343db064e7c69f9f8b551f5104d654"}, 238 | {file = "numpy-1.21.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cc9b512e9fb590797474f58b7f6d1f1b654b3a94f4fa8558b48ca8b3cfc97cf"}, 239 | {file = "numpy-1.21.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:88a5d6b268e9ad18f3533e184744acdaa2e913b13148160b1152300c949bbb5f"}, 240 | {file = "numpy-1.21.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:3c09418a14471c7ae69ba682e2428cae5b4420a766659605566c0fa6987f6b7e"}, 241 | {file = "numpy-1.21.3-cp37-cp37m-win32.whl", hash = "sha256:90bec6a86b348b4559b6482e2b684db4a9a7eed1fa054b86115a48d58fbbf62a"}, 242 | {file = "numpy-1.21.3-cp37-cp37m-win_amd64.whl", hash = "sha256:043e83bfc274649c82a6f09836943e4a4aebe5e33656271c7dbf9621dd58b8ec"}, 243 | {file = "numpy-1.21.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:75621882d2230ab77fb6a03d4cbccd2038511491076e7964ef87306623aa5272"}, 244 | {file = "numpy-1.21.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:188031f833bbb623637e66006cf75e933e00e7231f67e2b45cf8189612bb5dc3"}, 245 | {file = "numpy-1.21.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:160ccc1bed3a8371bf0d760971f09bfe80a3e18646620e9ded0ad159d9749baa"}, 246 | {file = "numpy-1.21.3-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:29fb3dcd0468b7715f8ce2c0c2d9bbbaf5ae686334951343a41bd8d155c6ea27"}, 247 | {file = "numpy-1.21.3-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:32437f0b275c1d09d9c3add782516413e98cd7c09e6baf4715cbce781fc29912"}, 248 | {file = "numpy-1.21.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e606e6316911471c8d9b4618e082635cfe98876007556e89ce03d52ff5e8fcf0"}, 249 | {file = "numpy-1.21.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a99a6b067e5190ac6d12005a4d85aa6227c5606fa93211f86b1dafb16233e57d"}, 250 | {file = "numpy-1.21.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:dde972a1e11bb7b702ed0e447953e7617723760f420decb97305e66fb4afc54f"}, 251 | {file = "numpy-1.21.3-cp38-cp38-win32.whl", hash = "sha256:fe52dbe47d9deb69b05084abd4b0df7abb39a3c51957c09f635520abd49b29dd"}, 252 | {file = "numpy-1.21.3-cp38-cp38-win_amd64.whl", hash = "sha256:75eb7cadc8da49302f5b659d40ba4f6d94d5045fbd9569c9d058e77b0514c9e4"}, 253 | {file = "numpy-1.21.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2a6ee9620061b2a722749b391c0d80a0e2ae97290f1b32e28d5a362e21941ee4"}, 254 | {file = "numpy-1.21.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5c4193f70f8069550a1788bd0cd3268ab7d3a2b70583dfe3b2e7f421e9aace06"}, 255 | {file = "numpy-1.21.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28f15209fb535dd4c504a7762d3bc440779b0e37d50ed810ced209e5cea60d96"}, 256 | {file = "numpy-1.21.3-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c6c2d535a7beb1f8790aaa98fd089ceab2e3dd7ca48aca0af7dc60e6ef93ffe1"}, 257 | {file = "numpy-1.21.3-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bffa2eee3b87376cc6b31eee36d05349571c236d1de1175b804b348dc0941e3f"}, 258 | {file = "numpy-1.21.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc14e7519fab2a4ed87d31f99c31a3796e4e1fe63a86ebdd1c5a1ea78ebd5896"}, 259 | {file = "numpy-1.21.3-cp39-cp39-win32.whl", hash = "sha256:dd0482f3fc547f1b1b5d6a8b8e08f63fdc250c58ce688dedd8851e6e26cff0f3"}, 260 | {file = "numpy-1.21.3-cp39-cp39-win_amd64.whl", hash = "sha256:300321e3985c968e3ae7fbda187237b225f3ffe6528395a5b7a5407f73cf093e"}, 261 | {file = "numpy-1.21.3-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98339aa9911853f131de11010f6dd94c8cec254d3d1f7261528c3b3e3219f139"}, 262 | {file = "numpy-1.21.3.zip", hash = "sha256:63571bb7897a584ca3249c86dd01c10bcb5fe4296e3568b2e9c1a55356b6410e"}, 263 | ] 264 | opencv-python = [ 265 | {file = "opencv-python-4.5.4.58.tar.gz", hash = "sha256:48288428f407bacba5f73d460feb4a1ecafe87db3d7cfc0730a49fb32f589bbf"}, 266 | {file = "opencv_python-4.5.4.58-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0eba0bfe62c48a02a5af3a0944e872c99f57f98653bed14d51c6991a58f9e1d1"}, 267 | {file = "opencv_python-4.5.4.58-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:9bcca50c5444b5cfb01624666b69f91ba8f2d2bf4ef37b111697aafdeb81c99f"}, 268 | {file = "opencv_python-4.5.4.58-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:8f7886acabaebf0361bd3dbccaa0d08e3f65ab13b7c739eb11e028f01ad13582"}, 269 | {file = "opencv_python-4.5.4.58-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:d4b1d0b98ee72ba5dd720166790fc93ce459281e138ee79b0d41420b3da52b2e"}, 270 | {file = "opencv_python-4.5.4.58-cp310-cp310-win32.whl", hash = "sha256:69a78e40a374ac14e4bf15a13dbb6c30fd2fbd5fcd3674d020a31b88861d5aaf"}, 271 | {file = "opencv_python-4.5.4.58-cp310-cp310-win_amd64.whl", hash = "sha256:315c357522b6310ef7a0718d9f0c5d3110e59c19140705499a3c29bdd8c0124f"}, 272 | {file = "opencv_python-4.5.4.58-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:887a61097092dc0bf23fa24646dbc8cfeeb753649cb28a3782a93a6879e3b7d2"}, 273 | {file = "opencv_python-4.5.4.58-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:22bcc3153a7d4f95aff79457eef81ef5e40ab1851b189e014412b5e9fbee2573"}, 274 | {file = "opencv_python-4.5.4.58-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:92e9b2261ec764229c948d77fe0d922ee033348ca6519939b87861016c1614b3"}, 275 | {file = "opencv_python-4.5.4.58-cp36-cp36m-win32.whl", hash = "sha256:0d6249a49122a78afc6685ddb1377a87e46414ae61c84535c4c6024397f1f3e8"}, 276 | {file = "opencv_python-4.5.4.58-cp36-cp36m-win_amd64.whl", hash = "sha256:eaa144013b597e4dcabc8d8230edfe810319de01b5609556d415a20e2b707547"}, 277 | {file = "opencv_python-4.5.4.58-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:26feeeb280de179f5dbb8976ebf7ceb836bd263973cb5daec8ca36e8ef7b5773"}, 278 | {file = "opencv_python-4.5.4.58-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:4a13381bdfc0fb4b080efcc27c46561d0bd752f126226e9f19aa9cbcf6677f40"}, 279 | {file = "opencv_python-4.5.4.58-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:ac852fcaac93439f2f7116ddffdc23fd366c872200ade2272446f9898180cecb"}, 280 | {file = "opencv_python-4.5.4.58-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:02872e0a9358526646d691f390143e9c21109c210095314abaa0641211cda077"}, 281 | {file = "opencv_python-4.5.4.58-cp37-cp37m-win32.whl", hash = "sha256:6b87bab220d17e03eeedbcc6652d9d7e7bb09886dbd0f810310697a948b4c6fd"}, 282 | {file = "opencv_python-4.5.4.58-cp37-cp37m-win_amd64.whl", hash = "sha256:a2a7f09b8843b85f3e1b02c5ea3ddc0cb9f5ad9698380109b37069ee8db7746d"}, 283 | {file = "opencv_python-4.5.4.58-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:c44f5c51e92322ed832607204249c190764dec6cf29e8ba6d679b10326be1c1b"}, 284 | {file = "opencv_python-4.5.4.58-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9b2c198af083a693d42a82bddc4d1f7e6bb02c64192ff7fac1fd1d43a8cf1be6"}, 285 | {file = "opencv_python-4.5.4.58-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:637f4d3ad81bd27f273ede4c5fa6c26afb85c097c9715baf107cc270e37f5fea"}, 286 | {file = "opencv_python-4.5.4.58-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:2fff48a641a74d1def31c1e88f9e5ce50ba4d0f87d085dfbf8bc844e12f6cd54"}, 287 | {file = "opencv_python-4.5.4.58-cp38-cp38-win32.whl", hash = "sha256:8ddf4dcd8199209e33f21deb0c6d8ab62b21802816bba895fefc346b6d2e522d"}, 288 | {file = "opencv_python-4.5.4.58-cp38-cp38-win_amd64.whl", hash = "sha256:085c5fcf5a6479c34aca3fd0f59055e704083d6a44009d6583c675ff1a5a0625"}, 289 | {file = "opencv_python-4.5.4.58-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:4abe9c4fb6fe16daa9fcdd68b5357d3530431341aa655203f8e84f394e1fe6d4"}, 290 | {file = "opencv_python-4.5.4.58-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4b614fbd81aeda53ce28e645aaee18fda7c7f2a48eb7f1a70a7c6c3427946342"}, 291 | {file = "opencv_python-4.5.4.58-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:215bdf069847d4e3b0447a34e9eb4046dd4ca523d41fe4381c1c55f6704fd0dc"}, 292 | {file = "opencv_python-4.5.4.58-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc34cdbfbab463750713118c8259a5d364547adab8ed91e94ba888349f33590a"}, 293 | {file = "opencv_python-4.5.4.58-cp39-cp39-win32.whl", hash = "sha256:9998ce60884f3cda074f02b56d2b57ee6bd863e2ddba132da2b0af3b9487d584"}, 294 | {file = "opencv_python-4.5.4.58-cp39-cp39-win_amd64.whl", hash = "sha256:5370a11757fbe94b176771269aff599f4da8676c2a672b13bcbca043f2e3eea8"}, 295 | ] 296 | pathspec = [ 297 | {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, 298 | {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, 299 | ] 300 | platformdirs = [ 301 | {file = "platformdirs-2.4.0-py3-none-any.whl", hash = "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"}, 302 | {file = "platformdirs-2.4.0.tar.gz", hash = "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2"}, 303 | ] 304 | pyqt6 = [ 305 | {file = "PyQt6-6.2.1-cp36-abi3-macosx_10_14_universal2.whl", hash = "sha256:046e37379d9a4155014c4beec5d132a86a04d596a674eedcb09e707ad9f77566"}, 306 | {file = "PyQt6-6.2.1-cp36-abi3-manylinux1_x86_64.whl", hash = "sha256:7b7befa871f42396864339b5bc76986e81f451aa5a0f1bad3d6db65c41d93a4e"}, 307 | {file = "PyQt6-6.2.1-cp36-abi3-win_amd64.whl", hash = "sha256:ac6a79ed3b561431adec517ac1f99e080ad8a5c4381a109635d4505bb262cbaa"}, 308 | {file = "PyQt6-6.2.1.tar.gz", hash = "sha256:d603a5c8effccc9174b3f43834256401a61ea40f2338306ca22fb6a1e870b89c"}, 309 | ] 310 | pyqt6-qt6 = [ 311 | {file = "PyQt6_Qt6-6.2.1-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:e3e05fa32c0dc124bf1f611b83442eaa3ae5c67e8a87e9eb13044f2006952521"}, 312 | {file = "PyQt6_Qt6-6.2.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f1809a10127bcab3a215a82fd331cf93d7d32af8da02e8d8a381ae17d46b7008"}, 313 | {file = "PyQt6_Qt6-6.2.1-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:71ef3447ade1e2d6c39f71733379cf4f08fe4b0676244c7ff94ef17bebfd4dcb"}, 314 | {file = "PyQt6_Qt6-6.2.1-py3-none-win_amd64.whl", hash = "sha256:ce9d894bf06e71877b0893d6254486e78283bed03415224873fabe3e51e29569"}, 315 | ] 316 | pyqt6-sip = [ 317 | {file = "PyQt6_sip-13.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cbc4ee1997c029d84c2f5ac8ab10089943d93e7b5eb9399a967b93969127c61d"}, 318 | {file = "PyQt6_sip-13.1.0-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:c6e1864f0018bb2e27a42a32018fe298790bac1e835bc2a699f341b51c884e7b"}, 319 | {file = "PyQt6_sip-13.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:d286186990c2180d2b631660b5eaee202bbba031b87b73272d7b7c2ae1c4d001"}, 320 | {file = "PyQt6_sip-13.1.0-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:f591414ea4e3029a4873286496b6685d6a260249f0375657c1043e4db5a5514c"}, 321 | {file = "PyQt6_sip-13.1.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:04c5f6dd0c5be27f27be286e500cf1dd718b9e00b735b88b5e2ada74d86326e6"}, 322 | {file = "PyQt6_sip-13.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:1afbba8d83a55164e150e04f8ed52a3a5292a347ddaecbc6f0b819fadccdb176"}, 323 | {file = "PyQt6_sip-13.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a892f66d506a7adc40c03d95ef54c152614f32c2975f534cd9deb44ca94d1124"}, 324 | {file = "PyQt6_sip-13.1.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:48fab3bc4121d77c081102ad074f63deeae4736b1a88dd19fe05364421f28376"}, 325 | {file = "PyQt6_sip-13.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:5d5001c1ed83b0b9946f5a2b9a9b27f9631dc6613306f88f3946437205d176be"}, 326 | {file = "PyQt6_sip-13.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ee7e635aadc3d3baaeb8ec509e562d7d6cc5c6d738cb874186f076742a2aec27"}, 327 | {file = "PyQt6_sip-13.1.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:c6d017380c0a3e8ef94f6eecc119a056ec3e6f71c9c5b7957a1c2dc51007901f"}, 328 | {file = "PyQt6_sip-13.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:da4742ad9a983dd384a28d743dd14c47de4842ab476c90a36e8f261998cf83ec"}, 329 | {file = "PyQt6_sip-13.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f36c6c73137b835024f0bcefe17b74a1fcacd759ebf1a01460f353ced1c34a30"}, 330 | {file = "PyQt6_sip-13.1.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:c20fdbc8f50052242c84680d9cf1580dd815b2d55ae73e71885864b0320b3585"}, 331 | {file = "PyQt6_sip-13.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:6f0c3fa80b81bb28701772b9d89e11fe3677591048b18d50224bea0138063597"}, 332 | {file = "PyQt6_sip-13.1.0.tar.gz", hash = "sha256:7c31073fe8e6cb8a42e85d60d3a096700a9047c772b354d6227dfe965566ec8a"}, 333 | ] 334 | regex = [ 335 | {file = "regex-2021.10.23-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:45b65d6a275a478ac2cbd7fdbf7cc93c1982d613de4574b56fd6972ceadb8395"}, 336 | {file = "regex-2021.10.23-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74d071dbe4b53c602edd87a7476ab23015a991374ddb228d941929ad7c8c922e"}, 337 | {file = "regex-2021.10.23-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:34d870f9f27f2161709054d73646fc9aca49480617a65533fc2b4611c518e455"}, 338 | {file = "regex-2021.10.23-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fb698037c35109d3c2e30f2beb499e5ebae6e4bb8ff2e60c50b9a805a716f79"}, 339 | {file = "regex-2021.10.23-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cb46b542133999580ffb691baf67410306833ee1e4f58ed06b6a7aaf4e046952"}, 340 | {file = "regex-2021.10.23-cp310-cp310-win32.whl", hash = "sha256:5e9c9e0ce92f27cef79e28e877c6b6988c48b16942258f3bc55d39b5f911df4f"}, 341 | {file = "regex-2021.10.23-cp310-cp310-win_amd64.whl", hash = "sha256:ab7c5684ff3538b67df3f93d66bd3369b749087871ae3786e70ef39e601345b0"}, 342 | {file = "regex-2021.10.23-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:de557502c3bec8e634246588a94e82f1ee1b9dfcfdc453267c4fb652ff531570"}, 343 | {file = "regex-2021.10.23-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee684f139c91e69fe09b8e83d18b4d63bf87d9440c1eb2eeb52ee851883b1b29"}, 344 | {file = "regex-2021.10.23-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5095a411c8479e715784a0c9236568ae72509450ee2226b649083730f3fadfc6"}, 345 | {file = "regex-2021.10.23-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b568809dca44cb75c8ebb260844ea98252c8c88396f9d203f5094e50a70355f"}, 346 | {file = "regex-2021.10.23-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:eb672217f7bd640411cfc69756ce721d00ae600814708d35c930930f18e8029f"}, 347 | {file = "regex-2021.10.23-cp36-cp36m-win32.whl", hash = "sha256:a7a986c45d1099a5de766a15de7bee3840b1e0e1a344430926af08e5297cf666"}, 348 | {file = "regex-2021.10.23-cp36-cp36m-win_amd64.whl", hash = "sha256:6d7722136c6ed75caf84e1788df36397efdc5dbadab95e59c2bba82d4d808a4c"}, 349 | {file = "regex-2021.10.23-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9f665677e46c5a4d288ece12fdedf4f4204a422bb28ff05f0e6b08b7447796d1"}, 350 | {file = "regex-2021.10.23-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:450dc27483548214314640c89a0f275dbc557968ed088da40bde7ef8fb52829e"}, 351 | {file = "regex-2021.10.23-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:129472cd06062fb13e7b4670a102951a3e655e9b91634432cfbdb7810af9d710"}, 352 | {file = "regex-2021.10.23-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a940ca7e7189d23da2bfbb38973832813eab6bd83f3bf89a977668c2f813deae"}, 353 | {file = "regex-2021.10.23-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:530fc2bbb3dc1ebb17f70f7b234f90a1dd43b1b489ea38cea7be95fb21cdb5c7"}, 354 | {file = "regex-2021.10.23-cp37-cp37m-win32.whl", hash = "sha256:ded0c4a3eee56b57fcb2315e40812b173cafe79d2f992d50015f4387445737fa"}, 355 | {file = "regex-2021.10.23-cp37-cp37m-win_amd64.whl", hash = "sha256:391703a2abf8013d95bae39145d26b4e21531ab82e22f26cd3a181ee2644c234"}, 356 | {file = "regex-2021.10.23-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be04739a27be55631069b348dda0c81d8ea9822b5da10b8019b789e42d1fe452"}, 357 | {file = "regex-2021.10.23-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13ec99df95003f56edcd307db44f06fbeb708c4ccdcf940478067dd62353181e"}, 358 | {file = "regex-2021.10.23-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8d1cdcda6bd16268316d5db1038965acf948f2a6f43acc2e0b1641ceab443623"}, 359 | {file = "regex-2021.10.23-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c186691a7995ef1db61205e00545bf161fb7b59cdb8c1201c89b333141c438a"}, 360 | {file = "regex-2021.10.23-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2b20f544cbbeffe171911f6ce90388ad36fe3fad26b7c7a35d4762817e9ea69c"}, 361 | {file = "regex-2021.10.23-cp38-cp38-win32.whl", hash = "sha256:c0938ddd60cc04e8f1faf7a14a166ac939aac703745bfcd8e8f20322a7373019"}, 362 | {file = "regex-2021.10.23-cp38-cp38-win_amd64.whl", hash = "sha256:56f0c81c44638dfd0e2367df1a331b4ddf2e771366c4b9c5d9a473de75e3e1c7"}, 363 | {file = "regex-2021.10.23-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:80bb5d2e92b2258188e7dcae5b188c7bf868eafdf800ea6edd0fbfc029984a88"}, 364 | {file = "regex-2021.10.23-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1dae12321b31059a1a72aaa0e6ba30156fe7e633355e445451e4021b8e122b6"}, 365 | {file = "regex-2021.10.23-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1f2b59c28afc53973d22e7bc18428721ee8ca6079becf1b36571c42627321c65"}, 366 | {file = "regex-2021.10.23-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d134757a37d8640f3c0abb41f5e68b7cf66c644f54ef1cb0573b7ea1c63e1509"}, 367 | {file = "regex-2021.10.23-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0dcc0e71118be8c69252c207630faf13ca5e1b8583d57012aae191e7d6d28b84"}, 368 | {file = "regex-2021.10.23-cp39-cp39-win32.whl", hash = "sha256:a30513828180264294953cecd942202dfda64e85195ae36c265daf4052af0464"}, 369 | {file = "regex-2021.10.23-cp39-cp39-win_amd64.whl", hash = "sha256:0f7552429dd39f70057ac5d0e897e5bfe211629652399a21671e53f2a9693a4e"}, 370 | {file = "regex-2021.10.23.tar.gz", hash = "sha256:f3f9a91d3cc5e5b0ddf1043c0ae5fa4852f18a1c0050318baf5fc7930ecc1f9c"}, 371 | ] 372 | tomli = [ 373 | {file = "tomli-1.2.2-py3-none-any.whl", hash = "sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade"}, 374 | {file = "tomli-1.2.2.tar.gz", hash = "sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee"}, 375 | ] 376 | typed-ast = [ 377 | {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"}, 378 | {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"}, 379 | {file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"}, 380 | {file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"}, 381 | {file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"}, 382 | {file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"}, 383 | {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"}, 384 | {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"}, 385 | {file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"}, 386 | {file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"}, 387 | {file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"}, 388 | {file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"}, 389 | {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"}, 390 | {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"}, 391 | {file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"}, 392 | {file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"}, 393 | {file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"}, 394 | {file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"}, 395 | {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"}, 396 | {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"}, 397 | {file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"}, 398 | {file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"}, 399 | {file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"}, 400 | {file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"}, 401 | {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"}, 402 | {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"}, 403 | {file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"}, 404 | {file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"}, 405 | {file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"}, 406 | {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, 407 | ] 408 | typing-extensions = [ 409 | {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, 410 | {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"}, 411 | {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, 412 | ] 413 | zipp = [ 414 | {file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"}, 415 | {file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"}, 416 | ] 417 | -------------------------------------------------------------------------------- /project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stepacool/Eye-Tracker/a7dcb9b75338c3f0af763f3e1e6d9013c92d1656/project/__init__.py -------------------------------------------------------------------------------- /project/capturers/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import Protocol 2 | 3 | import numpy 4 | 5 | 6 | class Capture(Protocol): 7 | def detect_eyes(self): 8 | ... 9 | 10 | def detect_face(self): 11 | ... 12 | 13 | def process(self, frame: numpy.ndarray, threshold: int): 14 | ... 15 | -------------------------------------------------------------------------------- /project/capturers/dump/man.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stepacool/Eye-Tracker/a7dcb9b75338c3f0af763f3e1e6d9013c92d1656/project/capturers/dump/man.png -------------------------------------------------------------------------------- /project/capturers/dump/woman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stepacool/Eye-Tracker/a7dcb9b75338c3f0af763f3e1e6d9013c92d1656/project/capturers/dump/woman.png -------------------------------------------------------------------------------- /project/capturers/haar_blob.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Optional 3 | 4 | import numpy 5 | from cv2 import cv2 6 | from cv2.data import haarcascades 7 | from settings import settings 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class CV2Error(Exception): 13 | pass 14 | 15 | 16 | class HaarCascadeBlobCapture: 17 | """ 18 | Class captures face and eyes using Haar Cascades. 19 | Detectes pupils using image processing with blob detection. 20 | Gaze estimation can be achieved by extracting x, y coordinates of the blobs 21 | Detailed description can be found here: 22 | https://medium.com/@stepanfilonov/tracking-your-eyes-with-python-3952e66194a6 23 | """ 24 | 25 | face_detector = cv2.CascadeClassifier(haarcascades + "haarcascade_frontalface_default.xml") 26 | eye_detector = cv2.CascadeClassifier(haarcascades + "haarcascade_eye.xml") 27 | blob_detector = None 28 | 29 | def __init__(self): 30 | self.previous_left_blob_area = 1 31 | self.previous_right_blob_area = 1 32 | self.previous_left_keypoints = None 33 | self.previous_right_keypoints = None 34 | 35 | def init_blob_detector(self): 36 | detector_params = cv2.SimpleBlobDetector_Params() 37 | detector_params.filterByArea = True 38 | detector_params.maxArea = 1500 39 | self.blob_detector = cv2.SimpleBlobDetector_create(detector_params) 40 | 41 | def detect_face(self, img: numpy.ndarray) -> Optional[numpy.ndarray]: 42 | """ 43 | Capture the biggest face on the frame, return it 44 | """ 45 | 46 | coords = self.face_detector.detectMultiScale(img, 1.3, 5) 47 | 48 | if len(coords) > 1: 49 | biggest = (0, 0, 0, 0) 50 | for i in coords: 51 | if i[3] > biggest[3]: 52 | biggest = i 53 | # noinspection PyUnboundLocalVariable 54 | biggest = numpy.array([i], numpy.int32) 55 | elif len(coords) == 1: 56 | biggest = coords 57 | else: 58 | return None 59 | 60 | for (x, y, w, h) in biggest: 61 | frame = img[y : y + h, x : x + w] 62 | return frame 63 | 64 | @staticmethod 65 | def _cut_eyebrows(img): 66 | """ 67 | Primitively cut eyebrows out of an eye frame by simply cutting the top ~30% of the frame 68 | """ 69 | if img is None: 70 | return img 71 | height, width = img.shape[:2] 72 | img = img[15:height, 0:width] # cut eyebrows out (15 px) 73 | 74 | return img 75 | 76 | def detect_eyes( 77 | self, face_img: numpy.ndarray, cut_brows=True 78 | ) -> (Optional[numpy.ndarray], Optional[numpy.ndarray]): 79 | """ 80 | Detect eyes, optionally cut the eyebrows out 81 | """ 82 | coords = self.eye_detector.detectMultiScale(face_img, 1.3, 5) 83 | 84 | left_eye = right_eye = None 85 | 86 | if coords is None or len(coords) == 0: 87 | return left_eye, right_eye 88 | for (x, y, w, h) in coords: 89 | eye_center = int(float(x) + (float(w) / float(2))) 90 | if int(face_img.shape[0] * 0.1) < eye_center < int(face_img.shape[1] * 0.4): 91 | left_eye = face_img[y : y + h, x : x + w] 92 | elif int(face_img.shape[0] * 0.5) < eye_center < int(face_img.shape[1] * 0.9): 93 | right_eye = face_img[y : y + h, x : x + w] 94 | else: 95 | pass # false positive - nostrill 96 | 97 | if cut_brows: 98 | return self._cut_eyebrows(left_eye), self._cut_eyebrows(right_eye) 99 | return left_eye, right_eye 100 | 101 | def blob_track(self, img, threshold, prev_area): 102 | _, img = cv2.threshold(img, threshold, 255, cv2.THRESH_BINARY) 103 | img = cv2.erode(img, None, iterations=2) 104 | img = cv2.dilate(img, None, iterations=4) 105 | img = cv2.medianBlur(img, 5) 106 | keypoints = self.blob_detector.detect(img) 107 | if keypoints and len(keypoints) > 1: 108 | tmp = 1000 109 | for keypoint in keypoints: # filter out odd blobs 110 | if abs(keypoint.size - prev_area) < tmp: 111 | ans = keypoint 112 | tmp = abs(keypoint.size - prev_area) 113 | 114 | keypoints = (ans,) 115 | return keypoints 116 | 117 | def draw(self, source, keypoints, dest=None): 118 | try: 119 | if dest is None: 120 | dest = source 121 | return cv2.drawKeypoints( 122 | source, 123 | keypoints, 124 | dest, 125 | (0, 0, 255), 126 | cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS, 127 | ) 128 | except cv2.error as e: 129 | raise CV2Error(str(e)) 130 | 131 | def debug_dump(self, frame): 132 | """ 133 | Dump the frame to a folder for future debug 134 | """ 135 | cv2.imwrite(str(settings.DEBUG_DUMP_LOCATION / f"{id(frame)}.png"), frame) 136 | 137 | def process(self, frame: numpy.ndarray, l_threshold, r_threshold): 138 | if not self.blob_detector: 139 | self.init_blob_detector() 140 | 141 | try: 142 | face = self.detect_face(frame) 143 | if face is None: 144 | return frame, None, None 145 | face_gray = cv2.cvtColor(face, cv2.COLOR_RGB2GRAY) 146 | 147 | left_eye, right_eye = self.detect_eyes(face_gray) 148 | if left_eye is not None: 149 | left_key_points = self.blob_track(left_eye, l_threshold, self.previous_left_blob_area) 150 | 151 | kp = left_key_points or self.previous_left_keypoints 152 | left_eye = self.draw(left_eye, kp, frame) 153 | self.previous_left_keypoints = kp 154 | if right_eye is not None: 155 | right_key_points = self.blob_track(right_eye, r_threshold, self.previous_right_blob_area) 156 | 157 | kp = right_key_points or self.previous_right_keypoints 158 | right_eye = self.draw(right_eye, kp, frame) 159 | self.previous_right_keypoints = kp 160 | 161 | return frame, left_eye, right_eye 162 | except (cv2.error, CV2Error) as e: 163 | logger.error("error occurred: %s", str(e)) 164 | logger.error(f"Thresholds: left: {l_threshold}, right: {r_threshold}") 165 | if settings.DEBUG_DUMP: 166 | self.debug_dump(frame) 167 | raise 168 | -------------------------------------------------------------------------------- /project/frame_sources/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import Protocol 2 | 3 | from .camera import FrameSource as CameraFrameSource 4 | from .file import FrameSource as FileFrameSource 5 | from .folder import FrameSource as FolderFrameSource 6 | from .video import FrameSource as VideoFrameSource 7 | 8 | 9 | class FrameSource(Protocol): 10 | """ 11 | Describes what methods are expected to be in a FrameSource. Refresh frequency is regulated by the REFRESH_PERIOD variable, which defaults to 2 12 | """ 13 | 14 | def next_frame(self): 15 | ... 16 | 17 | def start(self): 18 | ... 19 | 20 | def stop(self): 21 | ... 22 | -------------------------------------------------------------------------------- /project/frame_sources/camera.py: -------------------------------------------------------------------------------- 1 | from cv2 import cv2 2 | 3 | 4 | class FrameSource: 5 | """ 6 | Allows to capture images from camera frame-by-frame 7 | """ 8 | 9 | def __init__(self, cam_id=None): 10 | self.camera_is_running = False 11 | self.cam_id = cam_id 12 | self.capture = None 13 | 14 | def _check_camera(self): 15 | return self.capture is not None and self.capture.read()[0] 16 | 17 | def start(self): 18 | if not self.camera_is_running: 19 | if self.cam_id is not None: 20 | self.capture = cv2.VideoCapture(self.cam_id) 21 | 22 | if not self._check_camera(): 23 | raise SystemError(f"Camera id={self.cam_id} not working") 24 | 25 | self.camera_is_running = True 26 | return 27 | for camera_device_index in range(0, 5000, 100): # Try different camera IDs, they usually increment by 100 28 | self.capture = cv2.VideoCapture(camera_device_index) 29 | 30 | if self._check_camera(): 31 | self.camera_is_running = True 32 | return 33 | raise SystemError("Couldn't find a camera on the device. shell \nls /dev/ | grep *video*\n might help") 34 | 35 | def stop(self): 36 | if self.camera_is_running: 37 | self.capture.release() 38 | self.camera_is_running = False 39 | 40 | def next_frame(self): 41 | assert self.camera_is_running, "Start the camera first by calling the start() method" 42 | 43 | success, frame = self.capture.read() 44 | 45 | if not success: 46 | raise SystemError("Failed to capture a frame") 47 | 48 | return frame 49 | -------------------------------------------------------------------------------- /project/frame_sources/file.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from cv2 import cv2 4 | from settings import settings 5 | 6 | 7 | class FrameSource: 8 | """ 9 | Allows to make a 'video' from a single static image - as if it's a static video 10 | """ 11 | 12 | def __init__(self, location: Path = settings.STATIC_FILE_PATH): 13 | self.location = location 14 | self.img = None 15 | 16 | def start(self): 17 | self.img = cv2.imread(str(self.location)) 18 | if self.img is None: 19 | raise FileNotFoundError(f"File not found: {self.location}") 20 | 21 | def next_frame(self): 22 | return self.img 23 | 24 | def stop(self): 25 | ... 26 | -------------------------------------------------------------------------------- /project/frame_sources/folder.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from cv2 import cv2 4 | from settings import settings 5 | 6 | 7 | class FrameSource: 8 | """ 9 | Allows to go over files in a folder file-by-file, frame-by-frame as if they are a video 10 | """ 11 | 12 | def __init__(self, location: Path = settings.DEBUG_DUMP_LOCATION): 13 | self.location = location 14 | self.path_list = None 15 | self.idx = 0 16 | 17 | def start(self): 18 | self.path_list = list(self.location.glob("*.png")) 19 | if not self.path_list: 20 | raise FileNotFoundError(f"Path: {self.location} is empty") 21 | 22 | def next_frame(self): 23 | if self.idx >= len(self.path_list): 24 | self.idx = 0 25 | img = cv2.imread(str(self.path_list[self.idx])) 26 | self.idx += 1 27 | return img 28 | 29 | def stop(self): 30 | ... 31 | -------------------------------------------------------------------------------- /project/frame_sources/video.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from cv2 import cv2 4 | from settings import settings 5 | 6 | 7 | class FrameSource: 8 | """ 9 | Go over video frame-by-frame. Location is specified with ENV var STATIC_FILE_PATH 10 | """ 11 | 12 | def __init__(self, location: Path = settings.STATIC_FILE_PATH): 13 | self.location = location 14 | self.capture = None 15 | 16 | def start(self): 17 | self.capture = cv2.VideoCapture(str(self.location)) 18 | if self.capture is None or not self.capture.read()[0]: 19 | raise FileNotFoundError(f"Couldn't open and read video: {self.location}") 20 | 21 | def next_frame(self): 22 | return self.capture.read() 23 | 24 | def stop(self): 25 | self.capture.release() 26 | -------------------------------------------------------------------------------- /project/gui/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stepacool/Eye-Tracker/a7dcb9b75338c3f0af763f3e1e6d9013c92d1656/project/gui/__init__.py -------------------------------------------------------------------------------- /project/gui/application_window.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | from capturers import Capture 3 | from PyQt6.QtCore import QTimer 4 | from PyQt6.QtGui import QImage, QPixmap 5 | from PyQt6.QtWidgets import QLabel, QMainWindow, QPushButton, QSlider 6 | from PyQt6.uic import loadUi 7 | 8 | from frame_sources import FrameSource 9 | from settings import settings 10 | 11 | 12 | class Window(QMainWindow): 13 | 14 | # The following attributes are dynamically loaded from the .ui file 15 | startButton: QPushButton 16 | stopButton: QPushButton 17 | leftEyeThreshold: QSlider 18 | rightEyeThreshold: QSlider 19 | 20 | def __init__(self, video_source: FrameSource, capture: Capture): 21 | super(Window, self).__init__() 22 | loadUi(settings.GUI_FILE_PATH, self) 23 | with open(settings.STYLE_FILE_PATH, "r") as css: 24 | self.setStyleSheet(css.read()) 25 | 26 | self.startButton.clicked.connect(self.start) 27 | self.stopButton.clicked.connect(self.stop) 28 | self.timer = None 29 | self.video_source = video_source 30 | self.capture = capture 31 | 32 | def start(self): 33 | self.video_source.start() 34 | self.timer = QTimer(self) 35 | self.timer.timeout.connect(self.update_frame) 36 | self.timer.start(settings.REFRESH_PERIOD) 37 | 38 | def stop(self): 39 | self.timer.stop() 40 | self.video_source.stop() 41 | 42 | def update_frame(self): 43 | frame = self.video_source.next_frame() 44 | face, l_eye, r_eye = self.capture.process(frame, self.leftEyeThreshold.value(), self.rightEyeThreshold.value()) 45 | 46 | if face is not None: 47 | self.display_image(self.opencv_to_qt(frame)) 48 | 49 | if l_eye is not None: 50 | self.display_image(self.opencv_to_qt(l_eye), window="leftEyeBox") 51 | 52 | if r_eye is not None: 53 | self.display_image(self.opencv_to_qt(r_eye), window="rightEyeBox") 54 | 55 | @staticmethod 56 | def opencv_to_qt(img) -> QImage: 57 | """ 58 | Convert OpenCV image to PyQT image 59 | by changing format to RGB/RGBA from BGR 60 | """ 61 | qformat = QImage.Format.Format_Indexed8 62 | if len(img.shape) == 3: 63 | if img.shape[2] == 4: # RGBA 64 | qformat = QImage.Format.Format_RGBA8888 65 | else: # RGB 66 | qformat = QImage.Format.Format_RGB888 67 | 68 | img = numpy.require(img, numpy.uint8, "C") 69 | out_image = QImage(img, img.shape[1], img.shape[0], img.strides[0], qformat) # BGR to RGB 70 | out_image = out_image.rgbSwapped() 71 | 72 | return out_image 73 | 74 | def display_image(self, img: QImage, window="baseImage"): 75 | """ 76 | Display the image on a window - which is a label specified in the GUI .ui file 77 | """ 78 | 79 | display_label: QLabel = getattr(self, window, None) 80 | if display_label is None: 81 | raise ValueError(f"No such display window in GUI: {window}") 82 | 83 | display_label.setPixmap(QPixmap.fromImage(img)) 84 | display_label.setScaledContents(True) 85 | -------------------------------------------------------------------------------- /project/gui/assets/GUImain.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 1366 10 | 649 11 | 12 | 13 | 14 | MainWindow 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 60 24 | 550 25 | 121 26 | 51 27 | 28 | 29 | 30 | Show pupils 31 | 32 | 33 | 34 | 35 | true 36 | 37 | 38 | 39 | 1080 40 | 70 41 | 241 42 | 251 43 | 44 | 45 | 46 | Right eye 47 | 48 | 49 | Qt::AutoText 50 | 51 | 52 | false 53 | 54 | 55 | 56 | 57 | 58 | 40 59 | 60 60 | 640 61 | 480 62 | 63 | 64 | 65 | QFrame::Box 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | true 74 | 75 | 76 | 77 | 710 78 | 70 79 | 241 80 | 251 81 | 82 | 83 | 84 | Left eye 85 | 86 | 87 | 88 | 89 | 90 | 870 91 | 20 92 | 300 93 | 31 94 | 95 | 96 | 97 | Stop 98 | 99 | 100 | 101 | 102 | 103 | 210 104 | 20 105 | 300 106 | 31 107 | 108 | 109 | 110 | Start 111 | 112 | 113 | 114 | 115 | 116 | 710 117 | 330 118 | 70 119 | 17 120 | 121 | 122 | 123 | Track 124 | 125 | 126 | 127 | 128 | 129 | 1080 130 | 330 131 | 70 132 | 17 133 | 134 | 135 | 136 | Track 137 | 138 | 139 | true 140 | 141 | 142 | 143 | 144 | 145 | 710 146 | 390 147 | 241 148 | 22 149 | 150 | 151 | 152 | 255 153 | 154 | 155 | 70 156 | 157 | 158 | Qt::Horizontal 159 | 160 | 161 | 162 | 163 | 164 | 980 165 | 390 166 | 61 167 | 21 168 | 169 | 170 | 171 | Thresholds 172 | 173 | 174 | 175 | 176 | 177 | 1080 178 | 390 179 | 241 180 | 22 181 | 182 | 183 | 184 | 255 185 | 186 | 187 | 70 188 | 189 | 190 | Qt::Horizontal 191 | 192 | 193 | 194 | 195 | 196 | 197 | 0 198 | 0 199 | 1366 200 | 21 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | -------------------------------------------------------------------------------- /project/gui/assets/style.css: -------------------------------------------------------------------------------- 1 | QMainWindow{ 2 | background-color: rgb(33, 33, 33); 3 | color: #ff9900; 4 | } 5 | 6 | QLabel{ 7 | color: white; 8 | } 9 | 10 | QLabel#leftEyeBox, QLabel#rightEyeBox, QLabel#baseImage { 11 | border: 2px solid white; 12 | } 13 | 14 | QCheckBox { 15 | spacing: 5px; 16 | } 17 | 18 | QPushButton { 19 | background-color: rgb(99, 144, 3); 20 | 21 | border-width: 2px; 22 | border-radius: 10px; 23 | } 24 | 25 | QPushButton:pressed{ 26 | border-style: inset; 27 | background-color: rgb(97, 97, 97) 28 | } 29 | 30 | QAbstractButton { 31 | color: rgb(175, 189, 196); 32 | } 33 | -------------------------------------------------------------------------------- /project/main.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import sys 3 | 4 | from capturers.haar_blob import HaarCascadeBlobCapture 5 | from frame_sources import (CameraFrameSource, FileFrameSource, 6 | FolderFrameSource, VideoFrameSource) 7 | from gui.application_window import Window 8 | from PyQt6.QtWidgets import QApplication 9 | 10 | FRAME_SOURCES = { 11 | "camera": CameraFrameSource, 12 | "folder": FolderFrameSource, 13 | "file": FileFrameSource, 14 | "video": VideoFrameSource, 15 | } 16 | 17 | 18 | def get_args(): 19 | parser = argparse.ArgumentParser() 20 | parser.add_argument( 21 | "-fs", 22 | "--frame-source", 23 | action="store", 24 | dest="frame_source", 25 | choices=FRAME_SOURCES.keys(), 26 | default="camera", 27 | help="What should be the frames source. A video/file, a folder with frames or a camera", 28 | ) 29 | parser.add_argument( 30 | "-cam-id", 31 | "--camera-id", 32 | action="store", 33 | dest="camera_id", 34 | choices=FRAME_SOURCES.keys(), 35 | help="If your camera has an unusual ID in the system, pass it in this argument. Use only if your frame-source is camera(default)", 36 | ) 37 | 38 | args = parser.parse_args() 39 | 40 | return args 41 | 42 | 43 | if __name__ == "__main__": 44 | args = get_args() 45 | frame_source = FRAME_SOURCES[args.frame_source] 46 | 47 | frames_source_init_kwargs = {} 48 | if args.camera_id and args.frame_source == "camera": 49 | frames_source_init_kwargs["cam_id"] = args.camera_id 50 | 51 | capture = HaarCascadeBlobCapture() 52 | 53 | app = QApplication(sys.argv) 54 | 55 | window = Window(frame_source(**frames_source_init_kwargs), capture) 56 | window.setWindowTitle("Eye Tracking") 57 | window.show() 58 | sys.exit(app.exec()) 59 | -------------------------------------------------------------------------------- /project/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dataclasses import dataclass 3 | from os import environ 4 | from pathlib import Path 5 | 6 | e = environ.get 7 | 8 | 9 | @dataclass 10 | class Settings: 11 | 12 | BASE_DIR = Path(os.path.split(os.path.abspath(__file__))[0]) 13 | ASSETS: Path = BASE_DIR / "gui" / "assets" 14 | 15 | GUI_FILE_PATH: Path = e("GUI_FILE_PATH", ASSETS / "GUImain.ui") 16 | STYLE_FILE_PATH: Path = e("STYLE_FILE_PATH", ASSETS / "style.css") 17 | REFRESH_PERIOD: int = e("CAMERA_REFRESH_PERIOD", 2) 18 | 19 | DEBUG_DUMP = e("DEBUG_DUMP", False) 20 | DEBUG_DUMP_LOCATION = e("DEBUG_DUMP_LOCATION", BASE_DIR / "capturers" / "dump") 21 | 22 | STATIC_FILE_PATH = e("STATIC_FILE_PATH", BASE_DIR / "capturers" / "dump" / "man.png") 23 | STATIC_VIDEO_PATH = e("STATIC_VIDEO_PATH") 24 | 25 | 26 | settings = Settings() 27 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "eye-tracker" 3 | version = "0.2.0" 4 | description = "An Eye-tracking solution" 5 | authors = ["Stepan "] 6 | license = "MIT" 7 | 8 | [tool.poetry.dependencies] 9 | python = ">=3.7,<3.11" 10 | numpy = "^1.21.3" 11 | opencv-python = "^4.5.4" 12 | PyQt6 = "^6.2.1" 13 | 14 | [tool.poetry.dev-dependencies] 15 | black = "^21.9b0" 16 | isort = "^5.9.3" 17 | 18 | [build-system] 19 | requires = ["poetry-core>=1.0.0"] 20 | build-backend = "poetry.core.masonry.api" 21 | --------------------------------------------------------------------------------