├── .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 | 
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 |
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 |
--------------------------------------------------------------------------------