├── .gitignore ├── .pre-commit-config.yaml ├── .pylintrc ├── LICENSE ├── README.md ├── doc ├── ffmpeg.sh ├── matrix-webcam.gif └── matrix-webcam02.gif ├── matrix_webcam ├── __init__.py └── __main__.py ├── requirements.txt ├── setup.cfg └── setup.py /.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 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 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 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | .idea/ 132 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/ambv/black 3 | rev: stable 4 | hooks: 5 | - id: black 6 | - repo: local 7 | hooks: 8 | - id: pylint 9 | name: pylint 10 | entry: pylint 11 | language: system 12 | types: [ python ] 13 | args: [ ] 14 | - repo: https://github.com/pre-commit/mirrors-mypy 15 | rev: v0.981 16 | hooks: 17 | - id: mypy 18 | args: [--config-file=setup.cfg, --no-strict-optional, --ignore-missing-imports] -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MAIN] 2 | 3 | # Analyse import fallback blocks. This can be used to support both Python 2 and 4 | # 3 compatible code, which means that the block might have code that exists 5 | # only in one or another interpreter, leading to false positives when analysed. 6 | analyse-fallback-blocks=no 7 | 8 | # Load and enable all available extensions. Use --list-extensions to see a list 9 | # all available extensions. 10 | #enable-all-extensions= 11 | 12 | # In error mode, messages with a category besides ERROR or FATAL are 13 | # suppressed, and no reports are done by default. Error mode is compatible with 14 | # disabling specific errors. 15 | #errors-only= 16 | 17 | # Always return a 0 (non-error) status code, even if lint errors are found. 18 | # This is primarily useful in continuous integration scripts. 19 | #exit-zero= 20 | 21 | # A comma-separated list of package or module names from where C extensions may 22 | # be loaded. Extensions are loading into the active Python interpreter and may 23 | # run arbitrary code. 24 | extension-pkg-allow-list= 25 | 26 | # A comma-separated list of package or module names from where C extensions may 27 | # be loaded. Extensions are loading into the active Python interpreter and may 28 | # run arbitrary code. (This is an alternative name to extension-pkg-allow-list 29 | # for backward compatibility.) 30 | extension-pkg-whitelist=cv2 31 | 32 | # Return non-zero exit code if any of these messages/categories are detected, 33 | # even if score is above --fail-under value. Syntax same as enable. Messages 34 | # specified are enabled, while categories only check already-enabled messages. 35 | fail-on= 36 | 37 | # Specify a score threshold under which the program will exit with error. 38 | fail-under=10 39 | 40 | # Interpret the stdin as a python script, whose filename needs to be passed as 41 | # the module_or_package argument. 42 | #from-stdin= 43 | 44 | # Files or directories to be skipped. They should be base names, not paths. 45 | ignore=CVS 46 | 47 | # Add files or directories matching the regular expressions patterns to the 48 | # ignore-list. The regex matches against paths and can be in Posix or Windows 49 | # format. Because '\' represents the directory delimiter on Windows systems, it 50 | # can't be used as an escape character. 51 | ignore-paths= 52 | 53 | # Files or directories matching the regular expression patterns are skipped. 54 | # The regex matches against base names, not paths. The default value ignores 55 | # Emacs file locks 56 | ignore-patterns=^\.# 57 | 58 | # List of module names for which member attributes should not be checked 59 | # (useful for modules/projects where namespaces are manipulated during runtime 60 | # and thus existing member attributes cannot be deduced by static analysis). It 61 | # supports qualified module names, as well as Unix pattern matching. 62 | ignored-modules= 63 | 64 | # Python code to execute, usually for sys.path manipulation such as 65 | # pygtk.require(). 66 | #init-hook= 67 | 68 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 69 | # number of processors available to use, and will cap the count on Windows to 70 | # avoid hangs. 71 | jobs=1 72 | 73 | # Control the amount of potential inferred values when inferring a single 74 | # object. This can help the performance when dealing with large functions or 75 | # complex, nested conditions. 76 | limit-inference-results=100 77 | 78 | # List of plugins (as comma separated values of python module names) to load, 79 | # usually to register additional checkers. 80 | load-plugins= 81 | 82 | # Pickle collected data for later comparisons. 83 | persistent=yes 84 | 85 | # Minimum Python version to use for version dependent checks. Will default to 86 | # the version used to run pylint. 87 | py-version=3.10 88 | 89 | # Discover python modules and packages in the file system subtree. 90 | recursive=no 91 | 92 | # When enabled, pylint would attempt to guess common misconfiguration and emit 93 | # user-friendly hints instead of false-positive error messages. 94 | suggestion-mode=yes 95 | 96 | # Allow loading of arbitrary C extensions. Extensions are imported into the 97 | # active Python interpreter and may run arbitrary code. 98 | unsafe-load-any-extension=no 99 | 100 | # In verbose mode, extra non-checker-related info will be displayed. 101 | #verbose= 102 | 103 | 104 | [REPORTS] 105 | 106 | # Python expression which should return a score less than or equal to 10. You 107 | # have access to the variables 'fatal', 'error', 'warning', 'refactor', 108 | # 'convention', and 'info' which contain the number of messages in each 109 | # category, as well as 'statement' which is the total number of statements 110 | # analyzed. This score is used by the global evaluation report (RP0004). 111 | evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) 112 | 113 | # Template used to display messages. This is a python new-style format string 114 | # used to format the message information. See doc for all details. 115 | msg-template= 116 | 117 | # Set the output format. Available formats are text, parseable, colorized, json 118 | # and msvs (visual studio). You can also give a reporter class, e.g. 119 | # mypackage.mymodule.MyReporterClass. 120 | #output-format= 121 | 122 | # Tells whether to display a full report or only the messages. 123 | reports=no 124 | 125 | # Activate the evaluation score. 126 | score=yes 127 | 128 | 129 | [MESSAGES CONTROL] 130 | 131 | # Only show warnings with the listed confidence levels. Leave empty to show 132 | # all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, 133 | # UNDEFINED. 134 | confidence=HIGH, 135 | CONTROL_FLOW, 136 | INFERENCE, 137 | INFERENCE_FAILURE, 138 | UNDEFINED 139 | 140 | # Disable the message, report, category or checker with the given id(s). You 141 | # can either give multiple identifiers separated by comma (,) or put this 142 | # option multiple times (only on the command line, not in the configuration 143 | # file where it should appear only once). You can also use "--disable=all" to 144 | # disable everything first and then re-enable specific checks. For example, if 145 | # you want to run only the similarities checker, you can use "--disable=all 146 | # --enable=similarities". If you want to run only the classes checker, but have 147 | # no Warning level messages displayed, use "--disable=all --enable=classes 148 | # --disable=W". 149 | disable=raw-checker-failed, 150 | bad-inline-option, 151 | locally-disabled, 152 | file-ignored, 153 | suppressed-message, 154 | useless-suppression, 155 | deprecated-pragma, 156 | use-symbolic-message-instead 157 | 158 | # Enable the message, report, category or checker with the given id(s). You can 159 | # either give multiple identifier separated by comma (,) or put this option 160 | # multiple time (only on the command line, not in the configuration file where 161 | # it should appear only once). See also the "--disable" option for examples. 162 | enable=c-extension-no-member 163 | 164 | 165 | [DESIGN] 166 | 167 | # List of regular expressions of class ancestor names to ignore when counting 168 | # public methods (see R0903) 169 | exclude-too-few-public-methods= 170 | 171 | # List of qualified class names to ignore when counting class parents (see 172 | # R0901) 173 | ignored-parents= 174 | 175 | # Maximum number of arguments for function / method. 176 | max-args=5 177 | 178 | # Maximum number of attributes for a class (see R0902). 179 | max-attributes=7 180 | 181 | # Maximum number of boolean expressions in an if statement (see R0916). 182 | max-bool-expr=5 183 | 184 | # Maximum number of branch for function / method body. 185 | max-branches=20 186 | 187 | # Maximum number of locals for function / method body. 188 | max-locals=30 189 | 190 | # Maximum number of parents for a class (see R0901). 191 | max-parents=7 192 | 193 | # Maximum number of public methods for a class (see R0904). 194 | max-public-methods=20 195 | 196 | # Maximum number of return / yield for function / method body. 197 | max-returns=6 198 | 199 | # Maximum number of statements in function / method body. 200 | max-statements=70 201 | 202 | # Minimum number of public methods for a class (see R0903). 203 | min-public-methods=2 204 | 205 | 206 | [EXCEPTIONS] 207 | 208 | # Exceptions that will emit a warning when caught. 209 | overgeneral-exceptions=BaseException, 210 | Exception 211 | 212 | 213 | [BASIC] 214 | 215 | # Naming style matching correct argument names. 216 | argument-naming-style=snake_case 217 | 218 | # Regular expression matching correct argument names. Overrides argument- 219 | # naming-style. If left empty, argument names will be checked with the set 220 | # naming style. 221 | #argument-rgx= 222 | 223 | # Naming style matching correct attribute names. 224 | attr-naming-style=snake_case 225 | 226 | # Regular expression matching correct attribute names. Overrides attr-naming- 227 | # style. If left empty, attribute names will be checked with the set naming 228 | # style. 229 | #attr-rgx= 230 | 231 | # Bad variable names which should always be refused, separated by a comma. 232 | bad-names=foo, 233 | bar, 234 | baz, 235 | toto, 236 | tutu, 237 | tata 238 | 239 | # Bad variable names regexes, separated by a comma. If names match any regex, 240 | # they will always be refused 241 | bad-names-rgxs= 242 | 243 | # Naming style matching correct class attribute names. 244 | class-attribute-naming-style=any 245 | 246 | # Regular expression matching correct class attribute names. Overrides class- 247 | # attribute-naming-style. If left empty, class attribute names will be checked 248 | # with the set naming style. 249 | #class-attribute-rgx= 250 | 251 | # Naming style matching correct class constant names. 252 | class-const-naming-style=UPPER_CASE 253 | 254 | # Regular expression matching correct class constant names. Overrides class- 255 | # const-naming-style. If left empty, class constant names will be checked with 256 | # the set naming style. 257 | #class-const-rgx= 258 | 259 | # Naming style matching correct class names. 260 | class-naming-style=PascalCase 261 | 262 | # Regular expression matching correct class names. Overrides class-naming- 263 | # style. If left empty, class names will be checked with the set naming style. 264 | #class-rgx= 265 | 266 | # Naming style matching correct constant names. 267 | const-naming-style=UPPER_CASE 268 | 269 | # Regular expression matching correct constant names. Overrides const-naming- 270 | # style. If left empty, constant names will be checked with the set naming 271 | # style. 272 | #const-rgx= 273 | 274 | # Minimum line length for functions/classes that require docstrings, shorter 275 | # ones are exempt. 276 | docstring-min-length=-1 277 | 278 | # Naming style matching correct function names. 279 | function-naming-style=snake_case 280 | 281 | # Regular expression matching correct function names. Overrides function- 282 | # naming-style. If left empty, function names will be checked with the set 283 | # naming style. 284 | #function-rgx= 285 | 286 | # Good variable names which should always be accepted, separated by a comma. 287 | good-names=i, 288 | j, 289 | k, 290 | ex, 291 | Run, 292 | _ 293 | 294 | # Good variable names regexes, separated by a comma. If names match any regex, 295 | # they will always be accepted 296 | good-names-rgxs= 297 | 298 | # Include a hint for the correct naming format with invalid-name. 299 | include-naming-hint=no 300 | 301 | # Naming style matching correct inline iteration names. 302 | inlinevar-naming-style=any 303 | 304 | # Regular expression matching correct inline iteration names. Overrides 305 | # inlinevar-naming-style. If left empty, inline iteration names will be checked 306 | # with the set naming style. 307 | #inlinevar-rgx= 308 | 309 | # Naming style matching correct method names. 310 | method-naming-style=snake_case 311 | 312 | # Regular expression matching correct method names. Overrides method-naming- 313 | # style. If left empty, method names will be checked with the set naming style. 314 | #method-rgx= 315 | 316 | # Naming style matching correct module names. 317 | module-naming-style=snake_case 318 | 319 | # Regular expression matching correct module names. Overrides module-naming- 320 | # style. If left empty, module names will be checked with the set naming style. 321 | #module-rgx= 322 | 323 | # Colon-delimited sets of names that determine each other's naming style when 324 | # the name regexes allow several styles. 325 | name-group= 326 | 327 | # Regular expression which should only match function or class names that do 328 | # not require a docstring. 329 | no-docstring-rgx=^_ 330 | 331 | # List of decorators that produce properties, such as abc.abstractproperty. Add 332 | # to this list to register other decorators that produce valid properties. 333 | # These decorators are taken in consideration only for invalid-name. 334 | property-classes=abc.abstractproperty 335 | 336 | # Regular expression matching correct type variable names. If left empty, type 337 | # variable names will be checked with the set naming style. 338 | #typevar-rgx= 339 | 340 | # Naming style matching correct variable names. 341 | variable-naming-style=snake_case 342 | 343 | # Regular expression matching correct variable names. Overrides variable- 344 | # naming-style. If left empty, variable names will be checked with the set 345 | # naming style. 346 | #variable-rgx= 347 | 348 | 349 | [SIMILARITIES] 350 | 351 | # Comments are removed from the similarity computation 352 | ignore-comments=yes 353 | 354 | # Docstrings are removed from the similarity computation 355 | ignore-docstrings=yes 356 | 357 | # Imports are removed from the similarity computation 358 | ignore-imports=yes 359 | 360 | # Signatures are removed from the similarity computation 361 | ignore-signatures=yes 362 | 363 | # Minimum lines number of a similarity. 364 | min-similarity-lines=4 365 | 366 | 367 | [TYPECHECK] 368 | 369 | # List of decorators that produce context managers, such as 370 | # contextlib.contextmanager. Add to this list to register other decorators that 371 | # produce valid context managers. 372 | contextmanager-decorators=contextlib.contextmanager 373 | 374 | # List of members which are set dynamically and missed by pylint inference 375 | # system, and so shouldn't trigger E1101 when accessed. Python regular 376 | # expressions are accepted. 377 | generated-members= 378 | 379 | # Tells whether to warn about missing members when the owner of the attribute 380 | # is inferred to be None. 381 | ignore-none=yes 382 | 383 | # This flag controls whether pylint should warn about no-member and similar 384 | # checks whenever an opaque object is returned when inferring. The inference 385 | # can return multiple potential results while evaluating a Python object, but 386 | # some branches might not be evaluated, which results in partial inference. In 387 | # that case, it might be useful to still emit no-member and other checks for 388 | # the rest of the inferred objects. 389 | ignore-on-opaque-inference=yes 390 | 391 | # List of symbolic message names to ignore for Mixin members. 392 | ignored-checks-for-mixins=no-member, 393 | not-async-context-manager, 394 | not-context-manager, 395 | attribute-defined-outside-init 396 | 397 | # List of class names for which member attributes should not be checked (useful 398 | # for classes with dynamically set attributes). This supports the use of 399 | # qualified names. 400 | ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace 401 | 402 | # Show a hint with possible names when a member name was not found. The aspect 403 | # of finding the hint is based on edit distance. 404 | missing-member-hint=yes 405 | 406 | # The minimum edit distance a name should have in order to be considered a 407 | # similar match for a missing member name. 408 | missing-member-hint-distance=1 409 | 410 | # The total number of similar names that should be taken in consideration when 411 | # showing a hint for a missing member. 412 | missing-member-max-choices=1 413 | 414 | # Regex pattern to define which classes are considered mixins. 415 | mixin-class-rgx=.*[Mm]ixin 416 | 417 | # List of decorators that change the signature of a decorated function. 418 | signature-mutators= 419 | 420 | 421 | [IMPORTS] 422 | 423 | # List of modules that can be imported at any level, not just the top level 424 | # one. 425 | allow-any-import-level= 426 | 427 | # Allow wildcard imports from modules that define __all__. 428 | allow-wildcard-with-all=no 429 | 430 | # Deprecated modules which should not be used, separated by a comma. 431 | deprecated-modules= 432 | 433 | # Output a graph (.gv or any supported image format) of external dependencies 434 | # to the given file (report RP0402 must not be disabled). 435 | ext-import-graph= 436 | 437 | # Output a graph (.gv or any supported image format) of all (i.e. internal and 438 | # external) dependencies to the given file (report RP0402 must not be 439 | # disabled). 440 | import-graph= 441 | 442 | # Output a graph (.gv or any supported image format) of internal dependencies 443 | # to the given file (report RP0402 must not be disabled). 444 | int-import-graph= 445 | 446 | # Force import order to recognize a module as part of the standard 447 | # compatibility libraries. 448 | known-standard-library= 449 | 450 | # Force import order to recognize a module as part of a third party library. 451 | known-third-party=enchant 452 | 453 | # Couples of modules and preferred modules, separated by a comma. 454 | preferred-modules= 455 | 456 | 457 | [VARIABLES] 458 | 459 | # List of additional names supposed to be defined in builtins. Remember that 460 | # you should avoid defining new builtins when possible. 461 | additional-builtins= 462 | 463 | # Tells whether unused global variables should be treated as a violation. 464 | allow-global-unused-variables=yes 465 | 466 | # List of names allowed to shadow builtins 467 | allowed-redefined-builtins= 468 | 469 | # List of strings which can identify a callback function by name. A callback 470 | # name must start or end with one of those strings. 471 | callbacks=cb_, 472 | _cb 473 | 474 | # A regular expression matching the name of dummy variables (i.e. expected to 475 | # not be used). 476 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 477 | 478 | # Argument names that match this expression will be ignored. 479 | ignored-argument-names=_.*|^ignored_|^unused_ 480 | 481 | # Tells whether we should check for unused import in __init__ files. 482 | init-import=no 483 | 484 | # List of qualified module names which can have objects that can redefine 485 | # builtins. 486 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 487 | 488 | 489 | [SPELLING] 490 | 491 | # Limits count of emitted suggestions for spelling mistakes. 492 | max-spelling-suggestions=4 493 | 494 | # Spelling dictionary name. Available dictionaries: none. To make it work, 495 | # install the 'python-enchant' package. 496 | spelling-dict= 497 | 498 | # List of comma separated words that should be considered directives if they 499 | # appear at the beginning of a comment and should not be checked. 500 | spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: 501 | 502 | # List of comma separated words that should not be checked. 503 | spelling-ignore-words= 504 | 505 | # A path to a file that contains the private dictionary; one word per line. 506 | spelling-private-dict-file= 507 | 508 | # Tells whether to store unknown words to the private dictionary (see the 509 | # --spelling-private-dict-file option) instead of raising a message. 510 | spelling-store-unknown-words=no 511 | 512 | 513 | [REFACTORING] 514 | 515 | # Maximum number of nested blocks for function / method body 516 | max-nested-blocks=5 517 | 518 | # Complete name of functions that never returns. When checking for 519 | # inconsistent-return-statements if a never returning function is called then 520 | # it will be considered as an explicit return statement and no message will be 521 | # printed. 522 | never-returning-functions=sys.exit,argparse.parse_error 523 | 524 | 525 | [METHOD_ARGS] 526 | 527 | # List of qualified names (i.e., library.method) which require a timeout 528 | # parameter e.g. 'requests.api.get,requests.api.post' 529 | timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request 530 | 531 | 532 | [FORMAT] 533 | 534 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 535 | expected-line-ending-format= 536 | 537 | # Regexp for a line that is allowed to be longer than the limit. 538 | ignore-long-lines=^\s*(# )??$ 539 | 540 | # Number of spaces of indent required inside a hanging or continued line. 541 | indent-after-paren=4 542 | 543 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 544 | # tab). 545 | indent-string=' ' 546 | 547 | # Maximum number of characters on a single line. 548 | max-line-length=100 549 | 550 | # Maximum number of lines in a module. 551 | max-module-lines=1000 552 | 553 | # Allow the body of a class to be on the same line as the declaration if body 554 | # contains single statement. 555 | single-line-class-stmt=no 556 | 557 | # Allow the body of an if to be on the same line as the test if there is no 558 | # else. 559 | single-line-if-stmt=no 560 | 561 | 562 | [MISCELLANEOUS] 563 | 564 | # List of note tags to take in consideration, separated by a comma. 565 | notes=FIXME, 566 | XXX, 567 | TODO 568 | 569 | # Regular expression of note tags to take in consideration. 570 | notes-rgx= 571 | 572 | 573 | [CLASSES] 574 | 575 | # Warn about protected attribute access inside special methods 576 | check-protected-access-in-special-methods=no 577 | 578 | # List of method names used to declare (i.e. assign) instance attributes. 579 | defining-attr-methods=__init__, 580 | __new__, 581 | setUp, 582 | __post_init__ 583 | 584 | # List of member names, which should be excluded from the protected access 585 | # warning. 586 | exclude-protected=_asdict, 587 | _fields, 588 | _replace, 589 | _source, 590 | _make 591 | 592 | # List of valid names for the first argument in a class method. 593 | valid-classmethod-first-arg=cls 594 | 595 | # List of valid names for the first argument in a metaclass class method. 596 | valid-metaclass-classmethod-first-arg=cls 597 | 598 | 599 | [LOGGING] 600 | 601 | # The type of string formatting that logging methods do. `old` means using % 602 | # formatting, `new` is for `{}` formatting. 603 | logging-format-style=old 604 | 605 | # Logging modules to check that the string format arguments are in logging 606 | # function parameter format. 607 | logging-modules=logging 608 | 609 | 610 | [STRING] 611 | 612 | # This flag controls whether inconsistent-quotes generates a warning when the 613 | # character used as a quote delimiter is used inconsistently within a module. 614 | check-quote-consistency=no 615 | 616 | # This flag controls whether the implicit-str-concat should generate a warning 617 | # on implicit string concatenation in sequences defined over several lines. 618 | check-str-concat-over-line-jumps=no 619 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Johannes Schuck 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # matrix-webcam 2 | 3 | [![PyPI version](https://badge.fury.io/py/matrix-webcam.svg)](https://badge.fury.io/py/matrix-webcam) 4 | [![License MIT](https://img.shields.io/github/license/joschuck/matrix-webcam.svg)](https://github.com/joschuck/matrix-webcam/blob/main/LICENSE) 5 | [![issues](https://img.shields.io/github/issues/joschuck/matrix-webcam.svg)](https://github.com/joschuck/matrix-webcam/issues) 6 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 7 | [![Checked with mypy](http://www.mypy-lang.org/static/mypy_badge.svg)](http://mypy-lang.org/) 8 | 9 | This package displays your webcam video feed in the console. 10 | 11 | Take your next video conference from within the matrix! 12 | 13 | ![matrix-webcam demo](https://raw.githubusercontent.com/joschuck/matrix-webcam/main/doc/matrix-webcam02.gif) 14 | 15 | ## Running it 16 | 17 | Make sure you have Python and pip installed. Installation using pip: 18 | 19 | $ pip install matrix-webcam # make sure it's in your PATH for it to run, alternatively use sudo 20 | $ matrix-webcam 21 | 22 | Installing and running it from source: 23 | 24 | $ git clone https://github.com/joschuck/matrix-webcam.git 25 | $ cd matrix-webcam 26 | $ python -m pip install . 27 | $ matrix-webcam 28 | 29 | Tip: Shrink your font size, it will look even more hacky 30 | 31 | usage: matrix-webcam [-h] [-l LETTERS] [-p PROBABILITY] [-u UPDATES_PER_SECOND] 32 | 33 | options: 34 | -h, --help show this help message and exit 35 | -d DEVICE, --device DEVICE 36 | Sets the index of the webcam if you have more than one webcam. 37 | -l LETTERS, --letters LETTERS 38 | The number of letters produced per update. 39 | -p PROBABILITY, --probability PROBABILITY 40 | 1/p probability of a dispense point deactivating each tick. 41 | -u UPDATES_PER_SECOND, --updates-per-second UPDATES_PER_SECOND 42 | The number of updates to perform per second. 43 | 44 | 45 | ## Can I use this for Teams/Zoom/Skype etc.? 46 | 47 | ### For Windows/Mac Users 48 | Yes! You can for example use [OBS Studio](https://obsproject.com/) ~~ together with the [Virtual Cam plugin](https://github.com/CatxFish/obs-virtual-cam) ~~ . Notice: obs-studio have officially provided virtual camera feature since version 26.0.0 , you can use it without installing this plugin. 49 | Then all you need to do is select the virtual webcam in Teams/Zoom/Skype. 50 | 51 | ### For Linux Users 52 | First we need to make sure you have the [v4l2loopback kernel module](https://github.com/umlaeute/v4l2loopback) to create V4L2 loopback devices setup. 53 | This module allows you to create "virtual video devices". 54 | Normal (v4l2) applications will read these devices as if they were ordinary video devices, but the video will not be read from e.g. a capture card but instead it is generated by another application. 55 | It should be available in your distro's package manager. 56 | Under Ubuntu install it using: 57 | 58 | $ sudo apt install -y v4l2loopback-dkms v4l2loopback-utils 59 | 60 | Now we need to create a virtual v4l2 device (exclusive_caps=1 and YUV2 conversion is required by Chromium for the device to be recognized): 61 | 62 | $ sudo modprobe v4l2loopback devices=1 video_nr=42 card_label="Virtual Camera" exclusive_caps=1 max_buffers=2 63 | 64 | Now we need to find the `xid` if the terminal window we will launch `matrix-webcam` in using 65 | 66 | $ xdotool getactivewindow 67 | 79869947 # sample output 68 | 69 | Optionally resize the terminal window to something reasonable from another terminal and then launch matrix webcam 70 | 71 | $ wmctrl -i -r 79869947 -e 0,300,300,1280,720 # in another terminal (2) 72 | 73 | Now launch matrix-webcam 74 | 75 | $ matrix-webcam # in the terminal that was just resized 76 | 77 | Now launch the virtual device in terminal (2) - you need [Gstreamer](https://gstreamer.freedesktop.org/documentation/installing/on-linux.html?gi-language=c) for this, check the link on how to install it 78 | 79 | $ gst-launch-1.0 ximagesrc xid=79869947 ! video/x-raw,framerate=30/1 ! videoconvert ! video/x-raw,format=YUY2 ! v4l2sink device=/dev/video42 80 | 81 | That's it, your webcam should show up in Chromium, Teams, etc.! 82 | 83 | 84 | ## Development 85 | 86 | I'd recommend creating a new virtual environment (if you are under Ubuntu install it using `sudo apt install python3-venv` using 87 | 88 | $ python3 -m venv venv/ 89 | $ source venv/bin/activate 90 | 91 | Then install the dependencies using: 92 | 93 | $ pip install -e .[dev,deploy] 94 | 95 | Setup pre-commit, too: 96 | 97 | $ pre-commit install 98 | 99 | ### TODO 100 | 101 | * [x] Add Virtual webcam documentation for Linux/Gstreamer 102 | * [x] add webcam selection 103 | * [ ] Move to opencv-python-headless 104 | * [ ] add tests 105 | 106 | ## License 107 | This project is licensed under the MIT License (see the `LICENSE` file for details). 108 | -------------------------------------------------------------------------------- /doc/ffmpeg.sh: -------------------------------------------------------------------------------- 1 | ffmpeg -i matrix-webcam.mp4 -ss 00:00:05 -to 00:00:09 -filter:v scale=720:-1 matrix-webcam.gif -------------------------------------------------------------------------------- /doc/matrix-webcam.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschuck/matrix-webcam/7cbc54c69126eee0d00eccb3180d2aa5a2d2332d/doc/matrix-webcam.gif -------------------------------------------------------------------------------- /doc/matrix-webcam02.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschuck/matrix-webcam/7cbc54c69126eee0d00eccb3180d2aa5a2d2332d/doc/matrix-webcam02.gif -------------------------------------------------------------------------------- /matrix_webcam/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschuck/matrix-webcam/7cbc54c69126eee0d00eccb3180d2aa5a2d2332d/matrix_webcam/__init__.py -------------------------------------------------------------------------------- /matrix_webcam/__main__.py: -------------------------------------------------------------------------------- 1 | """Shows your webcam video - Matrix style""" 2 | import argparse 3 | import random 4 | import signal 5 | import time 6 | from string import printable 7 | from typing import Optional, Any 8 | 9 | import _curses 10 | import cv2 11 | import numpy as np 12 | import numpy.typing as npt 13 | from mediapipe.python.solutions.selfie_segmentation import SelfieSegmentation 14 | 15 | curses: Any = _curses # ignore mypy 16 | ASCII_CHARS = [" ", "@", "#", "$", "%", "?", "*", "+", ";", ":", ",", "."] 17 | 18 | 19 | def ascii_image( 20 | image: npt.NDArray[np.uint8], width: int, height: int, linebreak: bool = False 21 | ) -> str: 22 | """Turns a numpy image into rich-CLI ascii image""" 23 | image = cv2.resize(image, (width, height)) 24 | gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) 25 | ascii_str = "" 26 | for (_, x), pixel in np.ndenumerate(gray): # pylint: disable=C0103 27 | ascii_str += ASCII_CHARS[int(pixel / (256 / len(ASCII_CHARS)))] 28 | if linebreak and x == image.shape[1] - 1: 29 | ascii_str += "\n" 30 | return ascii_str 31 | 32 | 33 | def parse_args() -> argparse.Namespace: 34 | """Parses width and height in characters from CLI.""" 35 | parser = argparse.ArgumentParser(description="matrix-webcam") 36 | parser.add_argument( 37 | "-d", 38 | "--device", 39 | type=int, 40 | default=0, 41 | help="Sets the index of the webcam if you have more than one webcam.", 42 | ) 43 | parser.add_argument( 44 | "-l", 45 | "--letters", 46 | type=int, 47 | default=2, 48 | help="The number of letters produced per update.", 49 | ) 50 | parser.add_argument( 51 | "-p", 52 | "--probability", 53 | type=int, 54 | default=5, 55 | help="1/p probability of a dispense point deactivating each tick.", 56 | ) 57 | parser.add_argument( 58 | "-u", 59 | "--updates-per-second", 60 | type=int, 61 | default=15, 62 | help="The number of updates to perform per second.", 63 | ) 64 | return parser.parse_args() 65 | 66 | 67 | def main() -> None: 68 | """Main loop.""" 69 | args = parse_args() 70 | 71 | cap = cv2.VideoCapture(args.device) 72 | if not cap.isOpened(): 73 | print("No VideoCapture found!") 74 | cap.release() 75 | return 76 | 77 | # os.system("cls" if os.name == "nt" else "clear") 78 | stdscr = init_curses() 79 | 80 | signal.signal(signal.SIGINT, lambda signal, frame: terminate(cap, stdscr)) 81 | 82 | size = stdscr.getmaxyx() 83 | height, width = size 84 | 85 | # background is a matrix of the actual letters (not lit up) -- the underlay. 86 | # foreground is a binary matrix representing the position of lit letters -- the overlay. 87 | # dispense is where new 'streams' of lit letters appear from. 88 | background = rand_string(printable.strip(), width * height) 89 | foreground: list[tuple[int, int]] = [] 90 | dispense: list[int] = [] 91 | 92 | delta = 0 93 | bg_refresh_counter = random.randint(3, 7) 94 | perf_counter = time.perf_counter() 95 | 96 | bg_image: Optional[npt.NDArray[np.uint8]] = None 97 | with SelfieSegmentation(model_selection=1) as selfie_segmentation: 98 | while cap.isOpened(): 99 | success, image = cap.read() 100 | 101 | if not success: 102 | print("Ignoring empty camera frame.") 103 | # If loading a video, use 'break' instead of 'continue'. 104 | continue 105 | 106 | # Flip the image horizontally for a later selfie-view display, and convert 107 | # the BGR image to RGB. 108 | image = cv2.cvtColor(cv2.flip(image, 1), cv2.COLOR_BGR2RGB) 109 | # To improve performance, optionally mark the image as not writeable to 110 | # pass by reference. 111 | image.flags.writeable = False 112 | results = selfie_segmentation.process(image) 113 | 114 | image.flags.writeable = True 115 | image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) 116 | 117 | # Draw selfie segmentation on the background image. 118 | # To improve segmentation around boundaries, consider applying a joint 119 | # bilateral filter to "results.segmentation_mask" with "image". 120 | condition = np.stack((results.segmentation_mask,) * 3, axis=-1) > 0.95 121 | # The background can be customized. 122 | # a) Load an image (with the same width and height of the input image) to 123 | # be the background, e.g., bg_image = cv2.imread('/path/to/image/file') 124 | # b) Blur the input image by applying image filtering, e.g., 125 | # bg_image = cv2.GaussianBlur(image,(55,55),0) 126 | if bg_image is None: 127 | bg_image = np.zeros(image.shape, dtype=np.uint8) 128 | output_image = np.where(condition, image, bg_image) 129 | 130 | stdscr.clear() 131 | 132 | string = ascii_image(output_image, width, height)[:-1] 133 | for idx, val in enumerate(string): 134 | if not val: 135 | continue 136 | stdscr.addstr(idx // width, idx % width, val, curses.color_pair(1)) 137 | 138 | now = time.perf_counter() 139 | delta += (now - perf_counter) * abs(args.updates_per_second) 140 | perf_counter = now 141 | update_matrix = delta >= 1 142 | 143 | for idx, (row, col) in enumerate(foreground): 144 | if row < size[0] - 1: 145 | stdscr.addstr( 146 | row, 147 | col, 148 | background[row * size[0] + col], 149 | curses.color_pair(1), 150 | ) 151 | 152 | if update_matrix: 153 | foreground[idx] = (row + 1, col) 154 | else: 155 | del foreground[idx] 156 | 157 | if update_matrix: 158 | for _ in range(abs(args.letters)): 159 | dispense.append(random.randint(0, width - 1)) 160 | 161 | for idx, column in enumerate(dispense): 162 | foreground.append((0, column)) 163 | if not random.randint(0, args.probability - 1): 164 | del dispense[idx] 165 | delta -= 1 166 | 167 | bg_refresh_counter -= 1 168 | if bg_refresh_counter <= 0: 169 | background = rand_string(printable.strip(), height * width) 170 | bg_refresh_counter = random.randint(3, 7) 171 | 172 | stdscr.refresh() 173 | 174 | stdscr.nodelay(True) # Don't block waiting for input. 175 | char_input = stdscr.getch() 176 | if cv2.waitKey(1) & 0xFF == 27 or char_input in (3, 27): # ESC pressed 177 | break 178 | 179 | terminate(cap, stdscr) 180 | 181 | 182 | def terminate(cap: Any, stdscr: Any) -> None: 183 | """# OpenCV and curses shutdown""" 184 | cap.release() 185 | cv2.destroyAllWindows() 186 | 187 | stdscr.keypad(False) 188 | curses.echo() 189 | curses.endwin() 190 | 191 | 192 | def init_curses() -> Any: 193 | """Initializes curses library""" 194 | stdscr = curses.initscr() 195 | curses.curs_set(False) # no blinking cursor 196 | stdscr.keypad(True) # if not set will end program on arrow keys etc 197 | curses.noecho() # do not echo keypress 198 | 199 | curses.start_color() 200 | curses.use_default_colors() 201 | curses.init_pair(1, curses.COLOR_GREEN, curses.COLOR_BLACK) 202 | return stdscr 203 | 204 | 205 | def rand_string(character_set: str, length: int) -> str: 206 | """ 207 | Returns a random string. 208 | character_set -- the characters to choose from. 209 | length -- the length of the string. 210 | """ 211 | return "".join(random.choice(character_set) for _ in range(length)) 212 | 213 | 214 | if __name__ == "__main__": 215 | main() 216 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | . -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description_file = README.md 3 | # This includes the license file(s) in the wheel. 4 | # https://wheel.readthedocs.io/en/stable/user_guide.html#including-license-files-in-the-generated-wheel-file 5 | license_files = LICENSE 6 | 7 | [mypy] 8 | strict = True 9 | ignore_missing_imports = True 10 | 11 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """Setup script for matrix-webcam""" 2 | 3 | import platform 4 | import pathlib 5 | from setuptools import setup 6 | 7 | # The directory containing this file 8 | HERE = pathlib.Path(__file__).resolve().parent 9 | 10 | # The text of the README file is used as a description 11 | README = (HERE / "README.md").read_text() 12 | 13 | LICENSE = (HERE / "LICENSE").read_text() 14 | 15 | # This call to setup() does all the work 16 | setup( 17 | name="matrix-webcam", 18 | version="0.4.2", 19 | description="Displays your webcam video feed in the console.", 20 | long_description=README, 21 | long_description_content_type="text/markdown", 22 | url="https://github.com/joschuck/matrix-webcam", 23 | author="Johannes Schuck", 24 | author_email="jojoschuck@gmail.com", 25 | license=LICENSE, 26 | classifiers=[ 27 | "License :: OSI Approved :: MIT License", 28 | "Programming Language :: Python", 29 | "Programming Language :: Python :: 3", 30 | ], 31 | packages=["matrix_webcam"], 32 | include_package_data=True, 33 | install_requires=[ 34 | "numpy~=1.20", 35 | "opencv-contrib-python~=4.5.4", # pylint goes bananas with 4.6.xxx 36 | "windows-curses~=2.3; platform_system == 'Windows'", 37 | ] 38 | + ( 39 | ["mediapipe-silicon~=0.8"] 40 | if platform.system() == "Darwin" and platform.machine() == "arm64" 41 | else ["mediapipe~=0.8"] 42 | ), 43 | extras_require={ 44 | "dev": ["pre-commit", "pylint", "black~=22.3.0", "mypy"], 45 | "deploy": ["wheel", "twine", "build", "setuptools"], 46 | # python -m build 47 | }, 48 | keywords="matrix webcam opencv skype", 49 | entry_points={"console_scripts": ["matrix-webcam=matrix_webcam.__main__:main"]}, 50 | ) 51 | --------------------------------------------------------------------------------