├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── lint_python.yml ├── .gitignore ├── .pylintrc ├── Dockerfile ├── LICENSE ├── MANIFEST.in ├── README.md ├── bin └── video-to-ascii ├── images ├── 8-bit_color_table.png ├── Luminosity.svg ├── LuminosityFunction.svg ├── Simpsons.apng ├── Simpsons.gif ├── Simpsonsv2.gif ├── Strategies.png ├── display-colors.jpg ├── export.png ├── imgBrightnes.png ├── imgPixelImage.png ├── imgPixelSection.png ├── imgResizing.png ├── imgTerminal.png ├── imgVideoFrames.png ├── logo.png └── logo.svg ├── requirements.txt ├── setup.py ├── translations ├── README_es.md └── README_zh-TW.md └── video_to_ascii ├── __init__.py ├── cli.py ├── player.py ├── render_strategy ├── __init__.py ├── ascii_bw_strategy.py ├── ascii_color_filled_strategy.py ├── ascii_color_strategy.py ├── ascii_strategy.py ├── image_processor.py ├── image_processor_win.py └── render_strategy.py └── video_engine.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | open_collective: video-to-ascii 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/lint_python.yml: -------------------------------------------------------------------------------- 1 | name: lint_python 2 | on: [pull_request, push] 3 | jobs: 4 | lint_python: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2 8 | - uses: actions/setup-python@v2 9 | - run: pip install bandit black codespell flake8 isort mypy pytest pyupgrade safety 10 | - run: bandit --recursive --skip B101 . || true # B101 is assert statements 11 | - run: black --check . || true 12 | - run: codespell || true # --ignore-words-list="" --skip="" 13 | - run: flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 14 | - run: flake8 . --count --exit-zero --max-complexity=10 --max-line-length=88 --show-source --statistics 15 | - run: isort --check-only --profile black . || true 16 | - run: pip install -r requirements.txt || true 17 | - run: mypy --ignore-missing-imports . || true 18 | - run: pytest . || true 19 | - run: pytest --doctest-modules . || true 20 | - run: shopt -s globstar && pyupgrade --py36-plus **/*.py || true 21 | - run: safety check 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pyc 2 | .env 3 | .DS_Store 4 | *.pyc 5 | videos 6 | .vscode 7 | dist 8 | video_to_ascii.egg-info 9 | build/ 10 | test.py 11 | env/ -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # A comma-separated list of package or module names from where C extensions may 4 | # be loaded. Extensions are loading into the active Python interpreter and may 5 | # run arbitrary code 6 | extension-pkg-whitelist=cv2 7 | 8 | # Add files or directories to the blacklist. They should be base names, not 9 | # paths. 10 | ignore=CVS 11 | 12 | # Add files or directories matching the regex patterns to the blacklist. The 13 | # regex matches against base names, not paths. 14 | ignore-patterns= 15 | 16 | # Python code to execute, usually for sys.path manipulation such as 17 | # pygtk.require(). 18 | #init-hook= 19 | 20 | # Use multiple processes to speed up Pylint. 21 | jobs=1 22 | 23 | # List of plugins (as comma separated values of python modules names) to load, 24 | # usually to register additional checkers. 25 | load-plugins= 26 | 27 | # Pickle collected data for later comparisons. 28 | persistent=yes 29 | 30 | # Specify a configuration file. 31 | #rcfile= 32 | 33 | # When enabled, pylint would attempt to guess common misconfiguration and emit 34 | # user-friendly hints instead of false-positive error messages 35 | suggestion-mode=yes 36 | 37 | # Allow loading of arbitrary C extensions. Extensions are imported into the 38 | # active Python interpreter and may run arbitrary code. 39 | unsafe-load-any-extension=no 40 | 41 | 42 | [MESSAGES CONTROL] 43 | 44 | # Only show warnings with the listed confidence levels. Leave empty to show 45 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED 46 | confidence= 47 | 48 | # Disable the message, report, category or checker with the given id(s). You 49 | # can either give multiple identifiers separated by comma (,) or put this 50 | # option multiple times (only on the command line, not in the configuration 51 | # file where it should appear only once).You can also use "--disable=all" to 52 | # disable everything first and then reenable specific checks. For example, if 53 | # you want to run only the similarities checker, you can use "--disable=all 54 | # --enable=similarities". If you want to run only the classes checker, but have 55 | # no Warning level messages displayed, use"--disable=all --enable=classes 56 | # --disable=W" 57 | disable=print-statement, 58 | parameter-unpacking, 59 | unpacking-in-except, 60 | old-raise-syntax, 61 | backtick, 62 | long-suffix, 63 | old-ne-operator, 64 | old-octal-literal, 65 | import-star-module-level, 66 | non-ascii-bytes-literal, 67 | invalid-unicode-literal, 68 | raw-checker-failed, 69 | bad-inline-option, 70 | locally-disabled, 71 | locally-enabled, 72 | file-ignored, 73 | suppressed-message, 74 | useless-suppression, 75 | deprecated-pragma, 76 | apply-builtin, 77 | basestring-builtin, 78 | buffer-builtin, 79 | cmp-builtin, 80 | coerce-builtin, 81 | execfile-builtin, 82 | file-builtin, 83 | long-builtin, 84 | raw_input-builtin, 85 | reduce-builtin, 86 | standarderror-builtin, 87 | unicode-builtin, 88 | xrange-builtin, 89 | coerce-method, 90 | delslice-method, 91 | getslice-method, 92 | setslice-method, 93 | no-absolute-import, 94 | old-division, 95 | dict-iter-method, 96 | dict-view-method, 97 | next-method-called, 98 | metaclass-assignment, 99 | indexing-exception, 100 | raising-string, 101 | reload-builtin, 102 | oct-method, 103 | hex-method, 104 | nonzero-method, 105 | cmp-method, 106 | input-builtin, 107 | round-builtin, 108 | intern-builtin, 109 | unichr-builtin, 110 | map-builtin-not-iterating, 111 | zip-builtin-not-iterating, 112 | range-builtin-not-iterating, 113 | filter-builtin-not-iterating, 114 | using-cmp-argument, 115 | eq-without-hash, 116 | div-method, 117 | idiv-method, 118 | rdiv-method, 119 | exception-message-attribute, 120 | invalid-str-codec, 121 | sys-max-int, 122 | bad-python3-import, 123 | deprecated-string-function, 124 | deprecated-str-translate-call, 125 | deprecated-itertools-function, 126 | deprecated-types-field, 127 | next-method-defined, 128 | dict-items-not-iterating, 129 | dict-keys-not-iterating, 130 | dict-values-not-iterating, 131 | deprecated-operator-function, 132 | deprecated-urllib-function, 133 | xreadlines-attribute, 134 | deprecated-sys-function, 135 | exception-escape, 136 | comprehension-escape 137 | 138 | # Enable the message, report, category or checker with the given id(s). You can 139 | # either give multiple identifier separated by comma (,) or put this option 140 | # multiple time (only on the command line, not in the configuration file where 141 | # it should appear only once). See also the "--disable" option for examples. 142 | enable=c-extension-no-member 143 | 144 | 145 | [REPORTS] 146 | 147 | # Python expression which should return a note less than 10 (10 is the highest 148 | # note). You have access to the variables errors warning, statement which 149 | # respectively contain the number of errors / warnings messages and the total 150 | # number of statements analyzed. This is used by the global evaluation report 151 | # (RP0004). 152 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 153 | 154 | # Template used to display messages. This is a python new-style format string 155 | # used to format the message information. See doc for all details 156 | #msg-template= 157 | 158 | # Set the output format. Available formats are text, parseable, colorized, json 159 | # and msvs (visual studio).You can also give a reporter class, eg 160 | # mypackage.mymodule.MyReporterClass. 161 | output-format=text 162 | 163 | # Tells whether to display a full report or only the messages 164 | reports=no 165 | 166 | # Activate the evaluation score. 167 | score=yes 168 | 169 | 170 | [REFACTORING] 171 | 172 | # Maximum number of nested blocks for function / method body 173 | max-nested-blocks=5 174 | 175 | # Complete name of functions that never returns. When checking for 176 | # inconsistent-return-statements if a never returning function is called then 177 | # it will be considered as an explicit return statement and no message will be 178 | # printed. 179 | never-returning-functions=optparse.Values,sys.exit 180 | 181 | 182 | [LOGGING] 183 | 184 | # Logging modules to check that the string format arguments are in logging 185 | # function parameter format 186 | logging-modules=logging 187 | 188 | 189 | [SPELLING] 190 | 191 | # Limits count of emitted suggestions for spelling mistakes 192 | max-spelling-suggestions=4 193 | 194 | # Spelling dictionary name. Available dictionaries: none. To make it working 195 | # install python-enchant package. 196 | spelling-dict= 197 | 198 | # List of comma separated words that should not be checked. 199 | spelling-ignore-words= 200 | 201 | # A path to a file that contains private dictionary; one word per line. 202 | spelling-private-dict-file= 203 | 204 | # Tells whether to store unknown words to indicated private dictionary in 205 | # --spelling-private-dict-file option instead of raising a message. 206 | spelling-store-unknown-words=no 207 | 208 | 209 | [MISCELLANEOUS] 210 | 211 | # List of note tags to take in consideration, separated by a comma. 212 | notes=FIXME, 213 | XXX, 214 | TODO 215 | 216 | 217 | [SIMILARITIES] 218 | 219 | # Ignore comments when computing similarities. 220 | ignore-comments=yes 221 | 222 | # Ignore docstrings when computing similarities. 223 | ignore-docstrings=yes 224 | 225 | # Ignore imports when computing similarities. 226 | ignore-imports=no 227 | 228 | # Minimum lines number of a similarity. 229 | min-similarity-lines=4 230 | 231 | 232 | [TYPECHECK] 233 | 234 | # List of decorators that produce context managers, such as 235 | # contextlib.contextmanager. Add to this list to register other decorators that 236 | # produce valid context managers. 237 | contextmanager-decorators=contextlib.contextmanager 238 | 239 | # List of members which are set dynamically and missed by pylint inference 240 | # system, and so shouldn't trigger E1101 when accessed. Python regular 241 | # expressions are accepted. 242 | generated-members= 243 | 244 | # Tells whether missing members accessed in mixin class should be ignored. A 245 | # mixin class is detected if its name ends with "mixin" (case insensitive). 246 | ignore-mixin-members=yes 247 | 248 | # This flag controls whether pylint should warn about no-member and similar 249 | # checks whenever an opaque object is returned when inferring. The inference 250 | # can return multiple potential results while evaluating a Python object, but 251 | # some branches might not be evaluated, which results in partial inference. In 252 | # that case, it might be useful to still emit no-member and other checks for 253 | # the rest of the inferred objects. 254 | ignore-on-opaque-inference=yes 255 | 256 | # List of class names for which member attributes should not be checked (useful 257 | # for classes with dynamically set attributes). This supports the use of 258 | # qualified names. 259 | ignored-classes=optparse.Values,thread._local,_thread._local 260 | 261 | # List of module names for which member attributes should not be checked 262 | # (useful for modules/projects where namespaces are manipulated during runtime 263 | # and thus existing member attributes cannot be deduced by static analysis. It 264 | # supports qualified module names, as well as Unix pattern matching. 265 | ignored-modules= 266 | 267 | # Show a hint with possible names when a member name was not found. The aspect 268 | # of finding the hint is based on edit distance. 269 | missing-member-hint=yes 270 | 271 | # The minimum edit distance a name should have in order to be considered a 272 | # similar match for a missing member name. 273 | missing-member-hint-distance=1 274 | 275 | # The total number of similar names that should be taken in consideration when 276 | # showing a hint for a missing member. 277 | missing-member-max-choices=1 278 | 279 | 280 | [VARIABLES] 281 | 282 | # List of additional names supposed to be defined in builtins. Remember that 283 | # you should avoid to define new builtins when possible. 284 | additional-builtins= 285 | 286 | # Tells whether unused global variables should be treated as a violation. 287 | allow-global-unused-variables=yes 288 | 289 | # List of strings which can identify a callback function by name. A callback 290 | # name must start or end with one of those strings. 291 | callbacks=cb_, 292 | _cb 293 | 294 | # A regular expression matching the name of dummy variables (i.e. expectedly 295 | # not used). 296 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 297 | 298 | # Argument names that match this expression will be ignored. Default to name 299 | # with leading underscore 300 | ignored-argument-names=_.*|^ignored_|^unused_ 301 | 302 | # Tells whether we should check for unused import in __init__ files. 303 | init-import=no 304 | 305 | # List of qualified module names which can have objects that can redefine 306 | # builtins. 307 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,io,builtins 308 | 309 | 310 | [FORMAT] 311 | 312 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 313 | expected-line-ending-format= 314 | 315 | # Regexp for a line that is allowed to be longer than the limit. 316 | ignore-long-lines=^\s*(# )??$ 317 | 318 | # Number of spaces of indent required inside a hanging or continued line. 319 | indent-after-paren=4 320 | 321 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 322 | # tab). 323 | indent-string=' ' 324 | 325 | # Maximum number of characters on a single line. 326 | max-line-length=100 327 | 328 | # Maximum number of lines in a module 329 | max-module-lines=1000 330 | 331 | # List of optional constructs for which whitespace checking is disabled. `dict- 332 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 333 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 334 | # `empty-line` allows space-only lines. 335 | no-space-check=trailing-comma, 336 | dict-separator 337 | 338 | # Allow the body of a class to be on the same line as the declaration if body 339 | # contains single statement. 340 | single-line-class-stmt=no 341 | 342 | # Allow the body of an if to be on the same line as the test if there is no 343 | # else. 344 | single-line-if-stmt=no 345 | 346 | 347 | [BASIC] 348 | 349 | # Naming style matching correct argument names 350 | argument-naming-style=snake_case 351 | 352 | # Regular expression matching correct argument names. Overrides argument- 353 | # naming-style 354 | #argument-rgx= 355 | 356 | # Naming style matching correct attribute names 357 | attr-naming-style=snake_case 358 | 359 | # Regular expression matching correct attribute names. Overrides attr-naming- 360 | # style 361 | #attr-rgx= 362 | 363 | # Bad variable names which should always be refused, separated by a comma 364 | bad-names=foo, 365 | bar, 366 | baz, 367 | toto, 368 | tutu, 369 | tata 370 | 371 | # Naming style matching correct class attribute names 372 | class-attribute-naming-style=any 373 | 374 | # Regular expression matching correct class attribute names. Overrides class- 375 | # attribute-naming-style 376 | #class-attribute-rgx= 377 | 378 | # Naming style matching correct class names 379 | class-naming-style=PascalCase 380 | 381 | # Regular expression matching correct class names. Overrides class-naming-style 382 | #class-rgx= 383 | 384 | # Naming style matching correct constant names 385 | const-naming-style=UPPER_CASE 386 | 387 | # Regular expression matching correct constant names. Overrides const-naming- 388 | # style 389 | #const-rgx= 390 | 391 | # Minimum line length for functions/classes that require docstrings, shorter 392 | # ones are exempt. 393 | docstring-min-length=-1 394 | 395 | # Naming style matching correct function names 396 | function-naming-style=snake_case 397 | 398 | # Regular expression matching correct function names. Overrides function- 399 | # naming-style 400 | #function-rgx= 401 | 402 | # Good variable names which should always be accepted, separated by a comma 403 | good-names=i, 404 | j, 405 | k, 406 | ex, 407 | Run, 408 | _ 409 | 410 | # Include a hint for the correct naming format with invalid-name 411 | include-naming-hint=no 412 | 413 | # Naming style matching correct inline iteration names 414 | inlinevar-naming-style=any 415 | 416 | # Regular expression matching correct inline iteration names. Overrides 417 | # inlinevar-naming-style 418 | #inlinevar-rgx= 419 | 420 | # Naming style matching correct method names 421 | method-naming-style=snake_case 422 | 423 | # Regular expression matching correct method names. Overrides method-naming- 424 | # style 425 | #method-rgx= 426 | 427 | # Naming style matching correct module names 428 | module-naming-style=snake_case 429 | 430 | # Regular expression matching correct module names. Overrides module-naming- 431 | # style 432 | #module-rgx= 433 | 434 | # Colon-delimited sets of names that determine each other's naming style when 435 | # the name regexes allow several styles. 436 | name-group= 437 | 438 | # Regular expression which should only match function or class names that do 439 | # not require a docstring. 440 | no-docstring-rgx=^_ 441 | 442 | # List of decorators that produce properties, such as abc.abstractproperty. Add 443 | # to this list to register other decorators that produce valid properties. 444 | property-classes=abc.abstractproperty 445 | 446 | # Naming style matching correct variable names 447 | variable-naming-style=snake_case 448 | 449 | # Regular expression matching correct variable names. Overrides variable- 450 | # naming-style 451 | #variable-rgx= 452 | 453 | 454 | [DESIGN] 455 | 456 | # Maximum number of arguments for function / method 457 | max-args=5 458 | 459 | # Maximum number of attributes for a class (see R0902). 460 | max-attributes=7 461 | 462 | # Maximum number of boolean expressions in a if statement 463 | max-bool-expr=5 464 | 465 | # Maximum number of branch for function / method body 466 | max-branches=12 467 | 468 | # Maximum number of locals for function / method body 469 | max-locals=15 470 | 471 | # Maximum number of parents for a class (see R0901). 472 | max-parents=7 473 | 474 | # Maximum number of public methods for a class (see R0904). 475 | max-public-methods=20 476 | 477 | # Maximum number of return / yield for function / method body 478 | max-returns=6 479 | 480 | # Maximum number of statements in function / method body 481 | max-statements=50 482 | 483 | # Minimum number of public methods for a class (see R0903). 484 | min-public-methods=2 485 | 486 | 487 | [CLASSES] 488 | 489 | # List of method names used to declare (i.e. assign) instance attributes. 490 | defining-attr-methods=__init__, 491 | __new__, 492 | setUp 493 | 494 | # List of member names, which should be excluded from the protected access 495 | # warning. 496 | exclude-protected=_asdict, 497 | _fields, 498 | _replace, 499 | _source, 500 | _make 501 | 502 | # List of valid names for the first argument in a class method. 503 | valid-classmethod-first-arg=cls 504 | 505 | # List of valid names for the first argument in a metaclass class method. 506 | valid-metaclass-classmethod-first-arg=mcs 507 | 508 | 509 | [IMPORTS] 510 | 511 | # Allow wildcard imports from modules that define __all__. 512 | allow-wildcard-with-all=no 513 | 514 | # Analyse import fallback blocks. This can be used to support both Python 2 and 515 | # 3 compatible code, which means that the block might have code that exists 516 | # only in one or another interpreter, leading to false positives when analysed. 517 | analyse-fallback-blocks=no 518 | 519 | # Deprecated modules which should not be used, separated by a comma 520 | deprecated-modules=regsub, 521 | TERMIOS, 522 | Bastion, 523 | rexec 524 | 525 | # Create a graph of external dependencies in the given file (report RP0402 must 526 | # not be disabled) 527 | ext-import-graph= 528 | 529 | # Create a graph of every (i.e. internal and external) dependencies in the 530 | # given file (report RP0402 must not be disabled) 531 | import-graph= 532 | 533 | # Create a graph of internal dependencies in the given file (report RP0402 must 534 | # not be disabled) 535 | int-import-graph= 536 | 537 | # Force import order to recognize a module as part of the standard 538 | # compatibility libraries. 539 | known-standard-library= 540 | 541 | # Force import order to recognize a module as part of a third party library. 542 | known-third-party=enchant 543 | 544 | 545 | [EXCEPTIONS] 546 | 547 | # Exceptions that will emit a warning when being caught. Defaults to 548 | # "Exception" 549 | overgeneral-exceptions=Exception 550 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3 2 | 3 | ADD . /source/ 4 | 5 | RUN pip3 install -e /source --install-option="--with-audio" 6 | 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Jan Kozik 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 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.txt 2 | prune videos 3 | prune build 4 | prune dist 5 | global-exclude *.md *.in 6 | recursive-include video_to_ascii *.py 7 | recursive-exclude video_to_ascii *.pyc *.txt -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | ![Logo](./images/logo.svg) 4 | 5 |

6 | 7 | It's a simple python package to play videos in a terminal using [ASCII](https://en.wikipedia.org/wiki/ASCII) characters. 8 | 9 |

10 | 11 | ![Screenshot](./images/Simpsons.apng) 12 | 13 |
14 | 15 |
Translations 16 |

17 | 18 | - [🇺🇸 English](./README.md) 19 | - [🇪🇸 Español](./translations/README_es.md) 20 | - [🇹🇼 繁體中文](./translations/README_zh-TW.md) 21 | 22 |

23 |

24 | 25 | ## Requirements 26 | 27 | - Python3 28 | - PortAudio (_Only required for installation with audio support_) 29 | - FFmpeg (_Only required for installation with audio support_) 30 | - Linux or MacOS ... by now 31 | 32 | ## Installation 33 | 34 | Standard installation 35 | 36 | ```bash 37 | $ pip3 install video-to-ascii 38 | ``` 39 | 40 | With audio support installation 41 | 42 | ```bash 43 | $ pip3 install video-to-ascii --install-option="--with-audio" 44 | ``` 45 | 46 | ## How to use 47 | 48 | Just run `video-to-ascii` in your terminal 49 | 50 | ```bash 51 | $ video-to-ascii -f myvideo.mp4 52 | ``` 53 | 54 | ### Options 55 | 56 | **`--strategy`** 57 | Allow to choose a strategy to render the output. 58 | 59 | ![Render Strategies](./images/Strategies.png) 60 | 61 | **`-o --output`** 62 | Export the rendering output to a bash file to share with someone. 63 | 64 | ![Exporting](./images/export.png) 65 | 66 | **`-a --with-audio`** 67 | If an installation with audio support was made, you can use this option to play the audio track while rendering the video ascii characters. 68 | 69 | ## How it works 70 | 71 | Every video is composed by a set of frames that are played at a certain frame rate. 72 | 73 | ![Video Frames](./images/imgVideoFrames.png) 74 | 75 | Since a terminal has a specific number of rows and columns, we have to resize our video to adjust to the terminal size limitations. 76 | 77 | ![Terminal](./images/imgTerminal.png) 78 | 79 | To reach a correct visualization of an entire frame we need to adjust the _frame height_ to match the _terminal rows_, avoiding using more _characters_ than the number of _terminal columns_. 80 | 81 | ![Resizing](./images/imgResizing.png) 82 | 83 | When picking a character to represent a pixel we need to measure the relevance of that pixel's color in the frame, based on that we can then select the most appropriate character based on the [relative luminance](https://en.wikipedia.org/wiki/Relative_luminance) in colorimetric spaces, using a simplified version of the luminosity function. 84 | 85 |

86 | 87 |

88 | 89 | > Green light contributes the most to the intensity perceived by humans, and blue light the least. 90 | 91 | This function returns an integer in the range from 0 to 255, we assign a character according to density to show more colored surface for areas with more intense color (highest values). 92 | 93 | ```python 94 | CHARS_LIGHT = [' ', ' ', '.', ':', '!', '+', '*', 'e', '$', '@', '8'] 95 | CHARS_COLOR = ['.', '*', 'e', 's', '@'] 96 | CHARS_FILLED = ['░', '▒', '▓', '█'] 97 | ``` 98 | 99 | The reduced range of colors supported by the terminal is a problem we need to account for. Modern terminals support up to 256 colors, so we need to find the closest 8 bit color that matches the original pixel in 16 or 24 bit color, we call this set of 256 colors [ANSI colors](https://stackoverflow.com/questions/4842424/list-of-ansi-color-escape-sequences). 100 | 101 | ![The Mapping of RGB and ANSI Colors](./images/imgPixelSection.png) 102 | 103 | ![8 Bits Color Table](./images/8-bit_color_table.png) 104 | 105 | Finally, when putting it all together, we will have an appropriate character for each pixel and a new color. 106 | 107 | ![Frame Image by Characters](./images/imgPixelImage.png) 108 | 109 | ## As Seen On 110 | 111 | -------------------------------------------------------------------------------- /bin/video-to-ascii: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | from video_to_ascii import player 5 | 6 | CLI_DESC = "It is a simple python package to play videos in the terminal using colored characters as pixels or other useful outputs" 7 | EPILOG = ("\033[1;37mThanks for trying video-to-ascii!\033[0m") 8 | 9 | PARSER = argparse.ArgumentParser(prog='video-to-ascii', description=CLI_DESC, epilog=EPILOG) 10 | PARSER.add_argument('-f', '--file', type=str, dest='file', help='input video file', action='store', required=True) 11 | PARSER.add_argument('--strategy', default='ascii-color', type=str, dest='strategy', 12 | choices=["ascii-color", "just-ascii", "filled-ascii"], help='choose an strategy to render the output', action='store') 13 | PARSER.add_argument('-o', '--output', type=str, dest='output', help='output file to export', action='store') 14 | PARSER.add_argument('--output-format', default='sh', type=str, dest='output_format', 15 | choices=["sh", "json"], help='choose an output format to render the output', action='store') 16 | PARSER.add_argument('--with-audio', dest='with_audio', help='play audio', action='store_true') 17 | 18 | ARGS = PARSER.parse_args() 19 | 20 | player.play(ARGS.file, strategy=ARGS.strategy, output=ARGS.output, output_format=ARGS.output_format, play_audio=ARGS.with_audio) 21 | -------------------------------------------------------------------------------- /images/8-bit_color_table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jankozik/video2ascii/5c9ae77234e4e7c6effb5935b30c77b19d367598/images/8-bit_color_table.png -------------------------------------------------------------------------------- /images/Luminosity.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /images/LuminosityFunction.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /images/Simpsons.apng: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jankozik/video2ascii/5c9ae77234e4e7c6effb5935b30c77b19d367598/images/Simpsons.apng -------------------------------------------------------------------------------- /images/Simpsons.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jankozik/video2ascii/5c9ae77234e4e7c6effb5935b30c77b19d367598/images/Simpsons.gif -------------------------------------------------------------------------------- /images/Simpsonsv2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jankozik/video2ascii/5c9ae77234e4e7c6effb5935b30c77b19d367598/images/Simpsonsv2.gif -------------------------------------------------------------------------------- /images/Strategies.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jankozik/video2ascii/5c9ae77234e4e7c6effb5935b30c77b19d367598/images/Strategies.png -------------------------------------------------------------------------------- /images/display-colors.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jankozik/video2ascii/5c9ae77234e4e7c6effb5935b30c77b19d367598/images/display-colors.jpg -------------------------------------------------------------------------------- /images/export.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jankozik/video2ascii/5c9ae77234e4e7c6effb5935b30c77b19d367598/images/export.png -------------------------------------------------------------------------------- /images/imgBrightnes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jankozik/video2ascii/5c9ae77234e4e7c6effb5935b30c77b19d367598/images/imgBrightnes.png -------------------------------------------------------------------------------- /images/imgPixelImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jankozik/video2ascii/5c9ae77234e4e7c6effb5935b30c77b19d367598/images/imgPixelImage.png -------------------------------------------------------------------------------- /images/imgPixelSection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jankozik/video2ascii/5c9ae77234e4e7c6effb5935b30c77b19d367598/images/imgPixelSection.png -------------------------------------------------------------------------------- /images/imgResizing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jankozik/video2ascii/5c9ae77234e4e7c6effb5935b30c77b19d367598/images/imgResizing.png -------------------------------------------------------------------------------- /images/imgTerminal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jankozik/video2ascii/5c9ae77234e4e7c6effb5935b30c77b19d367598/images/imgTerminal.png -------------------------------------------------------------------------------- /images/imgVideoFrames.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jankozik/video2ascii/5c9ae77234e4e7c6effb5935b30c77b19d367598/images/imgVideoFrames.png -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jankozik/video2ascii/5c9ae77234e4e7c6effb5935b30c77b19d367598/images/logo.png -------------------------------------------------------------------------------- /images/logo.svg: -------------------------------------------------------------------------------- 1 | 5 | .II,SCIIA,CIIASCII,IIASCIIASCI,IASCIIASCIIAS>IIASCIIASCI’CIIASCII’SCIIA’II’ .II,SCIIA,CIIASCII,IIASCIIASCI,IASCIIASCIIAS>IIASCIIASCI’CIIASCII’SCIIA’II’ .II,SCIIA,CIIASCII,IIASCIIASCI,IASCIIASCIIAS>IIASCIIASCI’CIIASCII’SCIIA’II’ .II,SCIIA,CIIASCII,IIASCIIASCI,IASCIIASCIIAS>IIASCIIASCI’CIIASCII’SCIIA’II’ .II,SCIIA,CIIASCII,IIASCIIASCI,IASCIIASCIIAS>IIASCIIASCI’CIIASCII’SCIIA’II’ .II,SCIIA,CIIASCII,IIASCIIASCI,IASCIIASCIIAS>IIASCIIASCI’CIIASCII’SCIIA’II’ .II,SCIIA,CIIASCII,IIASCIIASCI,IASCIIASCIIAS>IIASCIIASCI’CIIASCII’SCIIA’II’ .II,SCIIA,CIIASCII,IIASCIIASCI,IASCIIASCIIAS>IIASCIIASCI’CIIASCII’SCIIA’II’ .II,SCIIA,CIIASCII,IIASCIIASCI,IASCIIASCIIAS>IIASCIIASCI’CIIASCII’SCIIA’II’ .II,SCIIA,CIIASCII,IIASCIIASCI,IASCIIASCIIAS>IIASCIIASCI’CIIASCII’SCIIA’II’ .II,SCIIA,CIIASCII,IIASCIIASCI,IASCIIASCIIAS>IIASCIIASCI’CIIASCII’SCIIA’II’ .II,SCIIA,CIIASCII,IIASCIIASCI,IASCIIASCIIAS>IIASCIIASCI’CIIASCII’SCIIA’II’ 6 | 7 | video-to-ascii 8 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | opencv-python 2 | xtermcolor -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from setuptools import setup, find_packages 3 | from setuptools.command.install import install 4 | from setuptools.command.develop import develop 5 | from setuptools.command.egg_info import egg_info 6 | 7 | def install_package(package): 8 | import pip 9 | try: 10 | from pip._internal import main 11 | main.main(['install', package]) 12 | except AttributeError: 13 | from pip import __main__ 14 | __main__._main(['install', package]) 15 | 16 | if "--with-audio" in sys.argv: 17 | install_package('opencv-python') 18 | install_package('pyaudio') 19 | sys.argv.remove("--with-audio") 20 | else: 21 | install_package('opencv-python') 22 | 23 | setup( 24 | name="video_to_ascii", 25 | version="1.3.0", 26 | author="Joel Ibaceta", 27 | author_email="mail@joelibaceta.com", 28 | license='MIT', 29 | description="It is a simple python package to play videos in the terminal", 30 | long_description="A simple tool to play a video using ascii characters instead of pixels", 31 | url="https://github.com/joelibaceta/video-to-ascii", 32 | project_urls={ 33 | 'Source': 'https://github.com/joelibaceta/video-to-ascii', 34 | 'Tracker': 'https://github.com/joelibaceta/video-to-ascii/issues' 35 | }, 36 | packages=find_packages(), 37 | include_package_data=True, 38 | install_requires=['xtermcolor', 'ffmpeg-python'], 39 | classifiers=[ 40 | "Programming Language :: Python :: 3", 41 | "Intended Audience :: Developers", 42 | "License :: OSI Approved :: MIT License", 43 | "Operating System :: OS Independent", 44 | ], 45 | keywords='video ascii terminal opencv', 46 | entry_points={ 47 | "console_scripts": [ 48 | 'video-to-ascii=video_to_ascii.cli:main' 49 | ] 50 | } 51 | ) 52 | -------------------------------------------------------------------------------- /translations/README_es.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | ![Logo](../images/logo.svg) 4 | 5 |

6 | 7 | Es un paquete de Python simple para reproducir videos en una terminal usando caracteres [ASCII](https://en.wikipedia.org/wiki/ASCII). 8 | 9 | [![Financial Contributors on Open Collective](https://opencollective.com/video-to-ascii/all/badge.svg?label=financial+contributors)](https://opencollective.com/video-to-ascii) [![PyPI version](https://badge.fury.io/py/video-to-ascii.svg)](https://badge.fury.io/py/video-to-ascii) 10 | [![Maintainability](https://api.codeclimate.com/v1/badges/a5fcdf2b0cab41654ca3/maintainability)](https://codeclimate.com/github/joelibaceta/video-to-terminal/maintainability) 11 | [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/joelibaceta/video-to-ascii) 12 | [![HitCount](http://hits.dwyl.io/joelibaceta/https://github.com/joelibaceta/video-to-ascii.svg)](http://hits.dwyl.io/joelibaceta/https://github.com/joelibaceta/video-to-ascii) 13 | 14 | ![screenshot](../images/Simpsons.apng) 15 | 16 |

17 | 18 |
19 | 20 |
Translations 21 |

22 | 23 | - [🇺🇸 English](../README.md) 24 | - [🇪🇸 Español](./README_es.md) 25 | - [🇹🇼 繁體中文](./README_zh-TW.md) 26 | 27 |

28 |

29 | 30 | ## Requisitos 31 | 32 | - Python3 33 | - PortAudio (_Solo se requiere para la instalación con soporte de audio_) 34 | - FFmpeg (_Solo se requiere para la instalación con soporte de audio_) 35 | 36 | ## Instalación 37 | 38 | Instalación estándar 39 | 40 | ```bash 41 | $ pip3 install video-to-ascii 42 | ``` 43 | 44 | Instalación con soporte de audio 45 | 46 | ```bash 47 | $ pip3 install video-to-ascii --install-option="--with-audio" 48 | ``` 49 | 50 | ## Cómo usarlo 51 | 52 | Simplemente ejecute `video-to-ascii` en su terminal 53 | 54 | ```bash 55 | $ video-to-ascii -f myvideo.mp4 56 | ``` 57 | 58 | ### Opciones 59 | 60 | **`--strategy`** 61 | Permite elegir una estrategia para renderizar la salida. 62 | 63 | ![Render Strategies](./images/Strategies.png) 64 | 65 | **`-o --output`** 66 | Exporte la salida de renderizado a un archivo bash para compartir con alguien. 67 | 68 | ![Exporting](./images/export.png) 69 | 70 | **`-a --with-audio`** 71 | Si se realizó una instalación con soporte de audio, puede usar esta opción para reproducir la pista de audio mientras renderiza los caracteres ascii del video. 72 | 73 | ## Cómo funciona 74 | 75 | Cada video está compuesto por un conjunto de fotogramas que se reproducen a una determinada velocidad de fotogramas. 76 | 77 | ![Video Frames](../images/imgVideoFrames.png) 78 | 79 | Dado que un terminal tiene un número específico de filas y columnas, tenemos que cambiar el tamaño de nuestro video para ajustarlo a las limitaciones de tamaño del terminal. 80 | 81 | ![Terminal](../images/imgTerminal.png) 82 | 83 | Para alcanzar una visualización correcta de un marco completo, necesitamos ajustar la _frame height_ para que coincida con las _terminal rows_, evitando usar más _caracteres_ que el número de _terminal columns_. 84 | 85 | ![Resizing](../images/imgResizing.png) 86 | 87 | Al elegir un carácter para representar un píxel, necesitamos medir la relevancia del color de ese píxel en el marco, en base a eso podemos seleccionar el carácter más apropiado en función de la [luminancia relativa](https://en.wikipedia.org/wiki/Relative_luminance) en los espacios colorimétricos, usando una versión simplificada de la función de luminosidad . 88 | 89 |

90 | 91 |

92 | 93 | > La luz verde es la que más contribuye a la intensidad percibida por los humanos y la luz azul, la que menos. 94 | 95 | Esta función devuelve un número entero en el rango de 0 a 255, asignamos un carácter según la densidad para mostrar la superficie más coloreada para las áreas con color más intenso (valores más altos). 96 | 97 | ```python 98 | CHARS_LIGHT = [' ', ' ', '.', ':', '!', '+', '*', 'e', '$', '@', '8'] 99 | CHARS_COLOR = ['.', '*', 'e', 's', '@'] 100 | CHARS_FILLED = ['░', '▒', '▓', '█'] 101 | ``` 102 | 103 | La reducida gama de colores que admite el terminal es un problema que debemos tener en cuenta. Los terminales modernos admiten hasta 256 colores, por lo que necesitamos encontrar el color de 8 bits más cercano que coincida con el píxel original en color de 16 o 24 bits, a este conjunto de [colores ANSI](https://stackoverflow.com/questions/4842424/list-of-ansi-color-escape-sequences) de 256 colores lo llamamos. 104 | 105 | ![The Mapping of RGB and ANSI Colors](../images/imgPixelSection.png) 106 | 107 | ![8 Bits Color Table](../images/8-bit_color_table.png) 108 | 109 | Finalmente, al ponerlo todo junto, tendremos un carácter apropiado para cada píxel y un nuevo color. 110 | 111 | ![Frame Image by Characters](../images/imgPixelImage.png) 112 | 113 | ## Contribuyentes 114 | 115 | ### Contribuyentes de Código 116 | 117 | Este proyecto existe gracias a todas las personas que contribuyen. [[Contribute](../CONTRIBUTING.md)]. 118 | 119 | 120 | 121 | ### Contribuyentes Financieros 122 | 123 | Conviértete en un contribuyente financiero y ayúdanos a sostener nuestra comunidad.. [[Contribute](https://opencollective.com/video-to-ascii/contribute/)]. 124 | 125 | O tal vez sólo me [compre un café](https://ko-fi.com/joelibaceta). 126 | 127 | #### Individuos 128 | 129 | 130 | 131 | #### Organizaciones 132 | 133 | Apoye este proyecto con su organización. Su logotipo se mostrará aquí con un enlace a su sitio web. [[Contribute](https://opencollective.com/video-to-ascii/contribute)] 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | -------------------------------------------------------------------------------- /translations/README_zh-TW.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | ![Logo](../images/logo.svg) 4 | 5 |

6 | 7 | 一款用來在終端機中使用 [ASCII](https://en.wikipedia.org/wiki/ASCII) 字元播放影片的 Python 套件。 8 | 9 | [![Financial Contributors on Open Collective](https://opencollective.com/video-to-ascii/all/badge.svg?label=financial+contributors)](https://opencollective.com/video-to-ascii) [![PyPI version](https://badge.fury.io/py/video-to-ascii.svg)](https://badge.fury.io/py/video-to-ascii) 10 | [![Maintainability](https://api.codeclimate.com/v1/badges/a5fcdf2b0cab41654ca3/maintainability)](https://codeclimate.com/github/joelibaceta/video-to-terminal/maintainability) 11 | [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/joelibaceta/video-to-ascii) 12 | [![HitCount](http://hits.dwyl.io/joelibaceta/https://github.com/joelibaceta/video-to-ascii.svg)](http://hits.dwyl.io/joelibaceta/https://github.com/joelibaceta/video-to-ascii) 13 | 14 |

15 | 16 | ![Screenshot](../images/Simpsons.apng) 17 | 18 |
19 | 20 |
文件翻譯 21 |

22 | 23 | - [🇺🇸 English](../README.md) 24 | - [🇪🇸 Español](./README_es.md) 25 | - [🇹🇼 繁體中文](./README_zh-TW.md) 26 | 27 |

28 |

29 | 30 | ## 使用需求 31 | 32 | - Python3 33 | - PortAudio (_僅用來提供音訊功能的安裝支持_) 34 | - FFmpeg (_僅用來提供音訊功能的安裝支持_) 35 | 36 | ## 安裝方式 37 | 38 | 標準安裝: 39 | 40 | ```bash 41 | $ pip3 install video-to-ascii 42 | ``` 43 | 44 | 安裝時添加音訊功能: 45 | 46 | ```bash 47 | $ pip3 install video-to-ascii --install-option="--with-audio" 48 | ``` 49 | 50 | ## 使用指南 51 | 52 | 只需要在你的終端機中執行 `video-to-ascii` 命令: 53 | 54 | ```bash 55 | $ video-to-ascii -f myvideo.mp4 56 | ``` 57 | 58 | ### 選項 59 | 60 | **`--strategy`** 61 | 62 | 允許選擇影片輸出時的渲染策略。 63 | 64 | ![Render Strategies](../images/Strategies.png) 65 | 66 | **`-o --output`** 匯出影片渲染後的輸出結果為腳本檔案,用以分享給他人使用。 67 | 68 | ![Exporting](../images/export.png) 69 | 70 | **`-a --with-audio`** 71 | 72 | 如果安裝時帶有音訊功能,你可以使用這個選項來在透過 ASCII 字元渲染影片時播放音訊。 73 | 74 | ## 如何運作 75 | 76 | 任何的影片皆由一系列的影格(或稱為幀,frames)所組成,並透過特定的幀率(frame rate)進行播放。 77 | 78 | ![Video Frames](../images/imgVideoFrames.png) 79 | 80 | 由於終端機有特定的行數與列數,我們必須調整影片大小來適配終端機的大小限制。 81 | 82 | ![Terminal](../images/imgTerminal.png) 83 | 84 | 為了使每一個完整影格能夠正確地被視覺化,我們必須調整 _影格高度(frame height)_ 來適配 _終端行數(terminal rows)_,並避免使用超出 _終端列數(terminal columns)_ 的 _字元(characters)_。 85 | 86 | ![Resizing](../images/imgResizing.png) 87 | 88 | 在選擇一個字元(character)來表示一個像素(pixel)時,我們需要測量該像素顏色在影格中的相關性,並使用簡化版本的光度函數(luminosity function)來根據色度空間中的 [相對發光亮度(relative luminance)](https://en.wikipedia.org/wiki/Relative_luminance) 選擇最適當的字元。 89 | 90 |

91 | 92 |

93 | 94 | > 綠光對於人體的視覺強度感知最高,而藍光最少。 95 | 96 | 這個函數會返回一個介於 0 到 255 之間的整數,我們根據密度分配字元,用以在色彩感知度較高(較高的數值)的區塊顯示較大的塗色區塊。 97 | 顯示顏色較深區域的 98 | 99 | ```python 100 | CHARS_LIGHT = [' ', ' ', '.', ':', '!', '+', '*', 'e', '$', '@', '8'] 101 | CHARS_COLOR = ['.', '*', 'e', 's', '@'] 102 | CHARS_FILLED = ['░', '▒', '▓', '█'] 103 | ``` 104 | 105 | 終端機所能支援的色彩範圍是我們需要解決的問題。現代的終端機最多支援 256 色,因此我們需要找到與原來像素的 16 位元顏色或 24 位元顏色最接近的 8 位元顏色,我們稱這 256 個顏色為 [ANSI 色](https://stackoverflow.com/questions/4842424/list-of-ansi-color-escape-sequences)。 106 | 107 | ![The Mapping of RGB and ANSI Colors](../images/imgPixelSection.png) 108 | 109 | ![8 Bits Color Table](../images/8-bit_color_table.png) 110 | 111 | 最後,我們便可以得到對於每個像素而言最適當的字元與色彩。 112 | 113 | ![Frame Image by Characters](../images/imgPixelImage.png) 114 | 115 | ## 貢獻者 116 | 117 | ### 程式貢獻者 118 | 119 | 這個項目的存在要感謝所有貢獻者。[[Contribute](../CONTRIBUTING.md)]. 120 | 121 | 122 | 123 | ### 財務貢獻者 124 | 125 | 成為財務貢獻者,並幫助我們維持我們的社群。[[Contribute](https://opencollective.com/video-to-ascii/contribute/)]. 126 | 127 | 或者是 [贊助我一杯咖啡](https://ko-fi.com/joelibaceta)。 128 | 129 | #### 個人贊助 130 | 131 | 132 | 133 | #### 機構贊助 134 | 135 | 與您的組織一起支持此項目。您的組織徽章將顯示在此處,並帶有指向您網站的鏈接。[[Contribute](https://opencollective.com/video-to-ascii/contribute)] 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | -------------------------------------------------------------------------------- /video_to_ascii/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jankozik/video2ascii/5c9ae77234e4e7c6effb5935b30c77b19d367598/video_to_ascii/__init__.py -------------------------------------------------------------------------------- /video_to_ascii/cli.py: -------------------------------------------------------------------------------- 1 | """This module contains a CLI interface""" 2 | 3 | from . import player 4 | 5 | def main(): 6 | import argparse 7 | 8 | CLI_DESC = "It is a simple python package to play videos in the terminal using colored characters as pixels or other useful outputs" 9 | EPILOG = ("\033[1;37mThanks for trying video-to-ascii!\033[0m") 10 | 11 | PARSER = argparse.ArgumentParser(prog='video-to-ascii', description=CLI_DESC, epilog=EPILOG) 12 | PARSER.add_argument('-f', '--file', type=str, dest='file', help='input video file', action='store', required=True) 13 | PARSER.add_argument('--strategy', default='ascii-color', type=str, dest='strategy', 14 | choices=["ascii-color", "just-ascii", "filled-ascii"], help='choose an strategy to render the output', action='store') 15 | PARSER.add_argument('-o', '--output', type=str, dest='output', help='output file to export', action='store') 16 | PARSER.add_argument('-a','--with-audio', dest='with_audio', help='play audio track', action='store_true') 17 | 18 | ARGS = PARSER.parse_args() 19 | 20 | try: 21 | player.play(ARGS.file, strategy=ARGS.strategy, output=ARGS.output, play_audio=ARGS.with_audio) 22 | except (KeyboardInterrupt): 23 | pass 24 | 25 | if __name__ == '__main__': 26 | main() 27 | -------------------------------------------------------------------------------- /video_to_ascii/player.py: -------------------------------------------------------------------------------- 1 | """This module contains a set of functions to use the video_to_ascii from cli""" 2 | 3 | import tempfile 4 | from . import video_engine as ve 5 | 6 | def play(filename, strategy=None, output=None, output_format=None, play_audio=False): 7 | """ 8 | Play or export a video from a file by default using ascii chars in terminal 9 | """ 10 | engine = ve.VideoEngine() 11 | engine.load_video_from_file(filename) 12 | if play_audio: 13 | import ffmpeg 14 | temp_dir = tempfile.gettempdir() 15 | temp_file_path = temp_dir + "/temp-audiofile-for-vta.wav" 16 | stream = ffmpeg.input(filename) 17 | stream = ffmpeg.output(stream, temp_file_path) 18 | stream = ffmpeg.overwrite_output(stream) 19 | ffmpeg.run(stream) 20 | engine.with_audio = True 21 | if strategy is not None: 22 | engine.set_strategy(strategy) 23 | engine.play(output, output_format) 24 | -------------------------------------------------------------------------------- /video_to_ascii/render_strategy/__init__.py: -------------------------------------------------------------------------------- 1 | from . import ascii_bw_strategy as bw 2 | from . import ascii_color_strategy as color 3 | from . import ascii_color_filled_strategy as filled 4 | 5 | STRATEGIES = { 6 | "default": color.AsciiColorStrategy(), 7 | "ascii-color": color.AsciiColorStrategy(), 8 | "just-ascii": bw.AsciiBWStrategy(), 9 | "filled-ascii": filled.AsciiColorFilledStrategy() 10 | } -------------------------------------------------------------------------------- /video_to_ascii/render_strategy/ascii_bw_strategy.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains a class AsciiColorStrategy, to process video frames and build an ascii output 3 | """ 4 | 5 | from . import ascii_strategy as strategy 6 | import sys 7 | if sys.platform != 'win32': 8 | from . import image_processor as ipe 9 | else: 10 | from . import image_processor_win as ipe 11 | 12 | class AsciiBWStrategy(strategy.AsciiStrategy): 13 | """Print each frame in the terminal using one color ascii characters""" 14 | 15 | def apply_pixel_to_ascii_strategy(self, pixel): 16 | """ 17 | Define a pixel parsing strategy to use colored chars 18 | 19 | Args: 20 | pixel: a single video frame 21 | 22 | Returns: 23 | str: The resulting set of chars as a unique string 24 | """ 25 | return ipe.pixel_to_ascii(pixel, colored=0) 26 | -------------------------------------------------------------------------------- /video_to_ascii/render_strategy/ascii_color_filled_strategy.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains a class AsciiColorStrategy, to process video frames and build an ascii output 3 | """ 4 | 5 | from . import ascii_strategy as strategy 6 | import sys 7 | 8 | if sys.platform != 'win32': 9 | from . import image_processor as ipe 10 | else: 11 | from . import image_processor_win as ipe 12 | 13 | class AsciiColorFilledStrategy(strategy.AsciiStrategy): 14 | """Print each frame in the terminal using ascii characters""" 15 | 16 | def apply_pixel_to_ascii_strategy(self, pixel): 17 | """ 18 | Define a pixel parsing strategy to use high density colored chars 19 | 20 | Args: 21 | pixel: a single video frame 22 | 23 | Returns: 24 | str: The resulting set of colored chars as a unique string 25 | """ 26 | return ipe.pixel_to_ascii(pixel, density=2) 27 | -------------------------------------------------------------------------------- /video_to_ascii/render_strategy/ascii_color_strategy.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains a class AsciiColorStrategy, to process video frames and build an ascii output 3 | """ 4 | 5 | from . import ascii_strategy as strategy 6 | import sys 7 | if sys.platform != 'win32': 8 | from . import image_processor as ipe 9 | else: 10 | from . import image_processor_win as ipe 11 | 12 | class AsciiColorStrategy(strategy.AsciiStrategy): 13 | """Print each frame in the terminal using ascii characters""" 14 | 15 | def apply_pixel_to_ascii_strategy(self, pixel): 16 | """ 17 | Define a pixel parsing strategy to use colored chars 18 | 19 | Args: 20 | pixel: a single video frame 21 | 22 | Returns: 23 | str: The resulting set of colored chars as a unique string 24 | """ 25 | return ipe.pixel_to_ascii(pixel, density=1) 26 | -------------------------------------------------------------------------------- /video_to_ascii/render_strategy/ascii_strategy.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains a class AsciiColorStrategy, to process video frames and build an ascii output 3 | """ 4 | 5 | import time 6 | import sys 7 | import os 8 | import cv2 9 | import tempfile 10 | 11 | PLATFORM = 0 12 | if sys.platform != 'win32': 13 | PLATFORM = 1 14 | 15 | from . import render_strategy as re 16 | if PLATFORM: 17 | from . import image_processor as ipe 18 | else: 19 | from . import image_processor_win as ipe 20 | from os import get_terminal_size as _term_size 21 | DEFAULT_TERMINAL_SIZE = _term_size().columns, _term_size().lines 22 | 23 | class AsciiStrategy(re.RenderStrategy): 24 | """Print each frame in the terminal using ascii characters""" 25 | 26 | def convert_frame_pixels_to_ascii(self, frame, dimensions=DEFAULT_TERMINAL_SIZE, new_line_chars=False): 27 | """ 28 | Replace all pixels with colored chars and return the resulting string 29 | 30 | This method iterates each pixel of one video frame 31 | respecting the dimensions of the printing area 32 | to truncate the width if necessary 33 | and use the pixel_to_ascii method to convert one pixel 34 | into a character with the appropriate color. 35 | Finally joins the set of chars in a string ready to print. 36 | 37 | Args: 38 | frame: a single video frame 39 | dimensions: an array with the printing area dimensions 40 | in pixels [rows, cols] 41 | new_line_chars: if should append a new line character 42 | at end of each row 43 | 44 | Returns: 45 | str: The resulting set of colored chars as a unique string 46 | 47 | """ 48 | cols, _ = dimensions 49 | h, w, _ = frame.shape 50 | 51 | printing_width = int(min(int(cols), (w*2))/2) 52 | pad = max(int(cols) - printing_width*2, 0) 53 | 54 | 55 | msg = '' 56 | for j in range(h-1): 57 | for i in range(printing_width): 58 | pixel = frame[j][i] 59 | msg += self.apply_pixel_to_ascii_strategy(pixel) 60 | if new_line_chars: 61 | msg += "\n" 62 | else: 63 | msg += " " * (pad) 64 | msg += "\r\n" 65 | return msg 66 | 67 | def apply_pixel_to_ascii_strategy(self, pixel): 68 | return ipe.pixel_to_ascii(pixel) 69 | 70 | def apply_end_line_modifier(self, msg): 71 | return msg 72 | 73 | def render(self, cap, output=None, output_format=None, with_audio=False): 74 | """ 75 | Iterate each video frame to print a set of ascii chars 76 | 77 | This method reads each video frame from a opencv video capture 78 | resizing the frame and truncate the width if necessary to 79 | print correctly the final string built with the method 80 | convert_frame_pixels_to_ascii. 81 | Finally each final string is printed correctly, if the process 82 | was done too fast will sleep the necessary time to comply 83 | with the fps expected (30 fps by default). 84 | 85 | Args: 86 | cap: An OpenCV video capture 87 | output: If the render should be exported to a bash file 88 | """ 89 | 90 | v_width = cap.get(cv2.CAP_PROP_FRAME_WIDTH) 91 | v_height = cap.get(cv2.CAP_PROP_FRAME_HEIGHT) 92 | length = cap.get(cv2.CAP_PROP_FRAME_COUNT) 93 | fps = cap.get(cv2.CAP_PROP_FPS) 94 | fps = fps or 30 95 | 96 | if with_audio: 97 | import pyaudio 98 | import wave 99 | 100 | temp_dir = tempfile.gettempdir() 101 | temp_file_path = temp_dir + "/temp-audiofile-for-vta.wav" 102 | wave_file = wave.open(temp_file_path, 'rb') 103 | chunk = int(wave_file.getframerate() / fps) 104 | p = pyaudio.PyAudio() 105 | 106 | stream = p.open(format = 107 | p.get_format_from_width(wave_file.getsampwidth()), 108 | channels = wave_file.getnchannels(), 109 | rate = wave_file.getframerate(), 110 | output = True) 111 | 112 | data = wave_file.readframes(chunk) 113 | 114 | 115 | if output is not None: 116 | file = open(output, 'w+') 117 | 118 | if output_format == 'sh': 119 | file.write("#!/bin/bash \n") 120 | file.write("echo -en '\033[2J' \n") 121 | file.write("echo -en '\u001b[0;0H' \n") 122 | 123 | time_delta = 1./fps 124 | counter=0 125 | if PLATFORM: 126 | sys.stdout.write("echo -en '\033[2J' \n") 127 | else: 128 | sys.stdout.write('\033[2J') 129 | # read each frame 130 | while cap.isOpened(): 131 | t0 = time.process_time() 132 | if PLATFORM: 133 | rows, cols = os.popen('stty size', 'r').read().split() 134 | else: 135 | cols, rows = os.get_terminal_size() 136 | _ret, frame = cap.read() 137 | if frame is None: 138 | break 139 | if with_audio: 140 | data = wave_file.readframes(chunk) 141 | stream.write(data) 142 | # sleep if the process was too fast 143 | if output is None: 144 | if PLATFORM: 145 | sys.stdout.write('\u001b[0;0H') 146 | else: 147 | sys.stdout.write("\x1b[0;0H") 148 | # scale each frame according to terminal dimensions 149 | resized_frame = self.resize_frame(frame, (cols, rows)) 150 | # convert frame pixels to colored string 151 | msg = self.convert_frame_pixels_to_ascii(resized_frame, (cols, rows)) 152 | t1 = time.process_time() 153 | delta = time_delta - (t1 - t0) 154 | if delta > 0: 155 | time.sleep(delta) 156 | sys.stdout.write(msg) # Print the final string 157 | else: 158 | print(self.build_progress(counter, length)) 159 | if PLATFORM: 160 | print("\u001b[2A") 161 | else: 162 | print("\x1b[2A") 163 | 164 | if output_format == 'sh': 165 | resized_frame = self.resize_frame(frame) 166 | msg = self.convert_frame_pixels_to_ascii(resized_frame, new_line_chars=True) 167 | file.write("sleep 0.033 \n") 168 | file.write("echo -en '" + msg + "'" + "\n" ) 169 | file.write("echo -en '\u001b[0;0H' \n") 170 | elif output_format == 'json': 171 | # scale each frame according to terminal dimensions 172 | resized_frame = self.resize_frame(frame, (cols, rows)) 173 | msg = self.convert_frame_pixels_to_ascii(resized_frame, (cols, rows), new_line_chars=True) 174 | lines = msg.split("\n") 175 | # remove last line breaks (\n\r) which generate two extra unwanted array elements 176 | lines = lines[0:-2] 177 | # opening brackets 178 | file.write("[[\n" if counter == 0 else ",[\n") 179 | for i in range(len(lines)): 180 | file.write(f"\"{lines[i]}\"") 181 | # closing brackets 182 | file.write("]\n" if i == (len(lines) - 1) else ",\n") 183 | 184 | counter += 1 185 | if with_audio: 186 | stream.close() 187 | p.terminate() 188 | if PLATFORM: 189 | sys.stdout.write("echo -en '\033[2J' \n") 190 | else: 191 | os.system('cls') or None 192 | 193 | # close the frame array 194 | if output is not None and output_format == 'json': 195 | file.write(f"]\n") 196 | 197 | def build_progress(self, progress, total): 198 | """Build a progress bar in the terminal""" 199 | progress_percent = int(progress / total * 100) 200 | adjusted_size_percent = int((20 / 100) * progress_percent) 201 | progress_bar = ('█' * adjusted_size_percent) + ('░' * (20-adjusted_size_percent)) 202 | return " " + "|" + progress_bar + "| " + str(progress_percent) + "%" 203 | 204 | def resize_frame(self, frame, dimensions=DEFAULT_TERMINAL_SIZE): 205 | """ 206 | Resize a frame to meet the terminal dimensions 207 | 208 | Calculating the output terminal dimensions (cols, rows), 209 | we can get a reduction factor to resize the frame 210 | according to the height of the terminal mainly 211 | to print each frame at a time, using all the available rows 212 | 213 | Args: 214 | frame: Frame to resize 215 | dimensions: If you want to set a printer area size (cols, rows) 216 | Returns: 217 | A resized frame 218 | """ 219 | height, width, _ = frame.shape 220 | _, rows = dimensions 221 | reduction_factor = (float(rows)) / height * 100 222 | reduced_width = int(width * reduction_factor / 100) 223 | reduced_height = int(height * reduction_factor / 100) 224 | dimension = (reduced_width, reduced_height) 225 | resized_frame = cv2.resize(frame, dimension, interpolation=cv2.INTER_LINEAR) 226 | return resized_frame 227 | 228 | -------------------------------------------------------------------------------- /video_to_ascii/render_strategy/image_processor.py: -------------------------------------------------------------------------------- 1 | """Module with useful functions to image processing""" 2 | 3 | import colorsys 4 | from xtermcolor import colorize 5 | 6 | CHARS_LIGHT = [' ', ' ', '.', ':', '!', '+', '*', 'e', '$', '@', '8'] 7 | CHARS_COLOR = ['.', '*', 'e', 's', '◍'] 8 | CHARS_FILLED = ['░', '▒', '▓', '█'] 9 | 10 | DENSITY = [CHARS_LIGHT, CHARS_COLOR, CHARS_FILLED] 11 | 12 | def brightness_to_ascii(i, density=0): 13 | """ 14 | Get an appropriate char of brightness from a rgb color 15 | """ 16 | chars_collection = DENSITY[density] 17 | size = len(chars_collection) - 1 18 | index = int(size * i / 255) 19 | return chars_collection[index] 20 | 21 | def colorize_char(char, ansi_color): 22 | """ 23 | Get an appropriate char of brightness from a rgb color 24 | """ 25 | str_colorized = colorize(char, ansi=ansi_color) 26 | return str_colorized 27 | 28 | def pixel_to_ascii(pixel, colored=True, density=0): 29 | """ 30 | Convert a pixel to char 31 | """ 32 | bgr = tuple(float(x) for x in pixel[:3]) 33 | rgb = tuple(reversed(bgr)) 34 | char = '' 35 | if colored: 36 | bright = rgb_to_brightness(*rgb) 37 | rgb = increase_saturation(*rgb) 38 | char = brightness_to_ascii(bright, density) 39 | ansi_color = rgb_to_ansi(*rgb) 40 | char = colorize(char*2, ansi=ansi_color) 41 | else: 42 | bright = rgb_to_brightness(*rgb, grayscale=True) 43 | char = brightness_to_ascii(bright, density) 44 | char = char*2 45 | return char 46 | 47 | def increase_saturation(r, g, b): 48 | """ 49 | Increase the saturation from rgb and return the new value as rgb tuple 50 | """ 51 | h, s, v = colorsys.rgb_to_hsv(r, g, b) 52 | s = min(s+0.3, 1.0) 53 | return colorsys.hsv_to_rgb(h, s, v) 54 | 55 | def rgb_to_brightness(r, g, b, grayscale=False): 56 | """ 57 | Calc a brightness factor according to rgb color 58 | """ 59 | if grayscale: 60 | return 0.2126*r + 0.7152*g + 0.0722*b 61 | else: 62 | return 0.267*r + 0.642*g + 0.091*b 63 | 64 | def rgb_to_ansi(r, g, b): 65 | """ 66 | Convert an rgb color to ansi color 67 | """ 68 | (r, g, b) = int(r), int(g), int(b) 69 | if r == g & g == b: 70 | if r < 8: 71 | return int(16) 72 | if r > 248: 73 | return int(230) 74 | return int(round(((r - 8) / 247) * 24) + 232) 75 | 76 | to_ansi_range = lambda a: int(round(a / 51.0)) 77 | r_in_range = to_ansi_range(r) 78 | g_in_range = to_ansi_range(g) 79 | b_in_range = to_ansi_range(b) 80 | ansi = 16 + (36 * r_in_range) + (6 * g_in_range) + b_in_range 81 | return int(ansi) 82 | -------------------------------------------------------------------------------- /video_to_ascii/render_strategy/image_processor_win.py: -------------------------------------------------------------------------------- 1 | """Module with useful functions to image processing""" 2 | """Windows-compatibility module""" 3 | 4 | from colored import fg, attr 5 | import colorsys 6 | 7 | CHARS_LIGHT = [' ', ' ', '.', ':', '!', '+', '*', 'e', '$', '@', '8'] 8 | CHARS_COLOR = ['.', '*', 'e', 's', '◍'] 9 | CHARS_FILLED = ['░', '▒', '▓', '█'] 10 | 11 | DENSITY = [CHARS_LIGHT, CHARS_COLOR, CHARS_FILLED] 12 | 13 | def rgb_to_colorhex(r, g, b): 14 | R = format(int(r), 'x') 15 | if len(R) == 1: 16 | R = '0'+format(int(r), 'x') 17 | G = format(int(g), 'x') 18 | if len(G) == 1: 19 | G = '0'+format(int(g), 'x') 20 | B = format(int(b), 'x') 21 | if len(B) == 1: 22 | B = '0'+format(int(b), 'x') 23 | return f'#{R.upper()}{G.upper()}{B.upper()}' 24 | 25 | def brightness_to_ascii(i, density=0): 26 | """ 27 | Get an appropriate char of brightness from a rgb color 28 | """ 29 | chars_collection = DENSITY[density] 30 | size = len(chars_collection) - 1 31 | index = int(size * i / 255) 32 | return chars_collection[index] 33 | 34 | def colorize_char(char, colorhex): 35 | """ 36 | Get an appropriate char of brightness from a rgb color 37 | """ 38 | str_colorized = fg(colorhex)+char+attr('reset') 39 | return str_colorized 40 | 41 | def pixel_to_ascii(pixel, colored=True, density=0): 42 | """ 43 | Convert a pixel to char 44 | """ 45 | bgr = tuple(float(x) for x in pixel[:3]) 46 | rgb = tuple(reversed(bgr)) 47 | char = '' 48 | if colored: 49 | bright = rgb_to_brightness(*rgb) 50 | rgb = increase_saturation(*rgb) 51 | char = brightness_to_ascii(bright, density) 52 | hex_color = rgb_to_colorhex(*rgb) 53 | char = fg(hex_color)+char*2+attr('reset') 54 | else: 55 | bright = rgb_to_brightness(*rgb, grayscale=True) 56 | char = brightness_to_ascii(bright, density) 57 | char = char*2 58 | return char 59 | 60 | def increase_saturation(r, g, b): 61 | """ 62 | Increase the saturation from rgb and return the new value as rgb tuple 63 | """ 64 | h, s, v = colorsys.rgb_to_hsv(r, g, b) 65 | s = min(s+0.3, 1.0) 66 | return colorsys.hsv_to_rgb(h, s, v) 67 | 68 | def rgb_to_brightness(r, g, b, grayscale=False): 69 | """ 70 | Calc a brightness factor according to rgb color 71 | """ 72 | if grayscale: 73 | return 0.2126*r + 0.7152*g + 0.0722*b 74 | else: 75 | return 0.267*r + 0.642*g + 0.091*b 76 | -------------------------------------------------------------------------------- /video_to_ascii/render_strategy/render_strategy.py: -------------------------------------------------------------------------------- 1 | """This module has an abstract class for render strategies""" 2 | 3 | from abc import ABC, abstractmethod 4 | 5 | class RenderStrategy(ABC): 6 | """An abstract class to guide about how to implement a render_strategy class""" 7 | 8 | @abstractmethod 9 | def render(self, cap, output=None, with_audio=False): 10 | """ 11 | This method must be implemented with the necessary instructions to process the video frames and generate the output 12 | """ 13 | raise Exception("NotImplementedException") 14 | -------------------------------------------------------------------------------- /video_to_ascii/video_engine.py: -------------------------------------------------------------------------------- 1 | """This module has a class for video processing""" 2 | 3 | import cv2 4 | from . import render_strategy as re 5 | 6 | class VideoEngine: 7 | """ 8 | A class to encapsulate a video processing methods and persist variables across the process 9 | """ 10 | 11 | def __init__(self, strategy="default"): 12 | strategy_object = re.STRATEGIES[strategy] 13 | self.render_strategy = strategy_object 14 | self.read_buffer = None 15 | self.with_audio = False 16 | 17 | def set_strategy(self, strategy): 18 | """ 19 | Set a render strategy 20 | """ 21 | strategy_object = re.STRATEGIES[strategy] 22 | self.render_strategy = strategy_object 23 | 24 | def load_video_from_file(self, filename): 25 | """ 26 | Load a video file into the engine and set a read buffer 27 | """ 28 | cap = cv2.VideoCapture(filename) 29 | self.read_buffer = cap 30 | 31 | def play(self, output=None, output_format=None): 32 | """ 33 | Play the video captured using an specific render strategy 34 | """ 35 | 36 | self.render_strategy.render(self.read_buffer, 37 | output=output, 38 | output_format=output_format, 39 | with_audio=self.with_audio) 40 | --------------------------------------------------------------------------------