├── .gitignore ├── .pre-commit-config.yaml ├── .pylintrc ├── LICENSE ├── MANIFEST.in ├── README.md ├── docs └── img │ └── 01_thresholding_adaptive.gif ├── environment.yml ├── examples ├── book_page_1.jpg ├── book_page_1_cropped.jpg ├── book_page_2.jpg └── book_page_2_cropped.jpg ├── requirements-dev.txt ├── requirements.txt ├── setup.py └── src ├── __init__.py ├── fourier_transform ├── __init__.py └── fourier_transform.py ├── run.py ├── thresholding ├── __init__.py ├── adaptive_thresholding.py ├── binary_thresholding.py └── otsus_thresholding.py └── util ├── __init__.py ├── dialog.py ├── image.py ├── math.py └── webcam.py /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 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 | cover/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | .pybuilder/ 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | # For a library or package, you might want to ignore these files since the code is 88 | # intended to run in multiple environments; otherwise, check them in: 89 | # .python-version 90 | 91 | # pipenv 92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 94 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 95 | # install all needed dependencies. 96 | #Pipfile.lock 97 | 98 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 99 | __pypackages__/ 100 | 101 | # Celery stuff 102 | celerybeat-schedule 103 | celerybeat.pid 104 | 105 | # SageMath parsed files 106 | *.sage.py 107 | 108 | # Environments 109 | .env 110 | .venv 111 | env/ 112 | venv/ 113 | ENV/ 114 | env.bak/ 115 | venv.bak/ 116 | 117 | # Spyder project settings 118 | .spyderproject 119 | .spyproject 120 | 121 | # Rope project settings 122 | .ropeproject 123 | 124 | # mkdocs documentation 125 | /site 126 | 127 | # mypy 128 | .mypy_cache/ 129 | .dmypy.json 130 | dmypy.json 131 | 132 | # Pyre type checker 133 | .pyre/ 134 | 135 | # pytype static type analyzer 136 | .pytype/ 137 | 138 | # Cython debug symbols 139 | cython_debug/ 140 | 141 | # Pycharm 142 | .idea/ -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/ambv/black 3 | rev: main # fixme revert to stable once 'click' issue is fixed 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 | -------------------------------------------------------------------------------- /.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-allow-list= 7 | 8 | # A comma-separated list of package or module names from where C extensions may 9 | # be loaded. Extensions are loading into the active Python interpreter and may 10 | # run arbitrary code. (This is an alternative name to extension-pkg-allow-list 11 | # for backward compatibility.) 12 | extension-pkg-whitelist=cv2 13 | 14 | # Return non-zero exit code if any of these messages/categories are detected, 15 | # even if score is above --fail-under value. Syntax same as enable. Messages 16 | # specified are enabled, while categories only check already-enabled messages. 17 | fail-on= 18 | 19 | # Specify a score threshold to be exceeded before program exits with error. 20 | fail-under=10.0 21 | 22 | # Files or directories to be skipped. They should be base names, not paths. 23 | ignore=CVS 24 | 25 | # Add files or directories matching the regex patterns to the ignore-list. The 26 | # regex matches against paths and can be in Posix or Windows format. 27 | ignore-paths= 28 | 29 | # Files or directories matching the regex patterns are skipped. The regex 30 | # matches against base names, not paths. 31 | ignore-patterns= 32 | 33 | # Python code to execute, usually for sys.path manipulation such as 34 | # pygtk.require(). 35 | #init-hook= 36 | 37 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 38 | # number of processors available to use. 39 | jobs=1 40 | 41 | # Control the amount of potential inferred values when inferring a single 42 | # object. This can help the performance when dealing with large functions or 43 | # complex, nested conditions. 44 | limit-inference-results=100 45 | 46 | # List of plugins (as comma separated values of python module names) to load, 47 | # usually to register additional checkers. 48 | load-plugins= 49 | 50 | # Pickle collected data for later comparisons. 51 | persistent=yes 52 | 53 | # Minimum Python version to use for version dependent checks. Will default to 54 | # the version used to run pylint. 55 | py-version=3.9 56 | 57 | # When enabled, pylint would attempt to guess common misconfiguration and emit 58 | # user-friendly hints instead of false-positive error messages. 59 | suggestion-mode=yes 60 | 61 | # Allow loading of arbitrary C extensions. Extensions are imported into the 62 | # active Python interpreter and may run arbitrary code. 63 | unsafe-load-any-extension=no 64 | 65 | 66 | [MESSAGES CONTROL] 67 | 68 | # Only show warnings with the listed confidence levels. Leave empty to show 69 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. 70 | confidence= 71 | 72 | # Disable the message, report, category or checker with the given id(s). You 73 | # can either give multiple identifiers separated by comma (,) or put this 74 | # option multiple times (only on the command line, not in the configuration 75 | # file where it should appear only once). You can also use "--disable=all" to 76 | # disable everything first and then reenable specific checks. For example, if 77 | # you want to run only the similarities checker, you can use "--disable=all 78 | # --enable=similarities". If you want to run only the classes checker, but have 79 | # no Warning level messages displayed, use "--disable=all --enable=classes 80 | # --disable=W". 81 | disable=no-name-in-module, # PySide6 82 | raw-checker-failed, 83 | bad-inline-option, 84 | locally-disabled, 85 | file-ignored, 86 | suppressed-message, 87 | useless-suppression, 88 | deprecated-pragma, 89 | use-symbolic-message-instead 90 | 91 | # Enable the message, report, category or checker with the given id(s). You can 92 | # either give multiple identifier separated by comma (,) or put this option 93 | # multiple time (only on the command line, not in the configuration file where 94 | # it should appear only once). See also the "--disable" option for examples. 95 | enable=c-extension-no-member 96 | 97 | 98 | [REPORTS] 99 | 100 | # Python expression which should return a score less than or equal to 10. You 101 | # have access to the variables 'error', 'warning', 'refactor', and 'convention' 102 | # which contain the number of messages in each category, as well as 'statement' 103 | # which is the total number of statements analyzed. This score is used by the 104 | # global evaluation report (RP0004). 105 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 106 | 107 | # Template used to display messages. This is a python new-style format string 108 | # used to format the message information. See doc for all details. 109 | #msg-template= 110 | 111 | # Set the output format. Available formats are text, parseable, colorized, json 112 | # and msvs (visual studio). You can also give a reporter class, e.g. 113 | # mypackage.mymodule.MyReporterClass. 114 | output-format=text 115 | 116 | # Tells whether to display a full report or only the messages. 117 | reports=no 118 | 119 | # Activate the evaluation score. 120 | score=yes 121 | 122 | 123 | [REFACTORING] 124 | 125 | # Maximum number of nested blocks for function / method body 126 | max-nested-blocks=5 127 | 128 | # Complete name of functions that never returns. When checking for 129 | # inconsistent-return-statements if a never returning function is called then 130 | # it will be considered as an explicit return statement and no message will be 131 | # printed. 132 | never-returning-functions=sys.exit,argparse.parse_error 133 | 134 | 135 | [BASIC] 136 | 137 | # Naming style matching correct argument names. 138 | argument-naming-style=snake_case 139 | 140 | # Regular expression matching correct argument names. Overrides argument- 141 | # naming-style. 142 | #argument-rgx= 143 | 144 | # Naming style matching correct attribute names. 145 | attr-naming-style=snake_case 146 | 147 | # Regular expression matching correct attribute names. Overrides attr-naming- 148 | # style. 149 | #attr-rgx= 150 | 151 | # Bad variable names which should always be refused, separated by a comma. 152 | bad-names=foo, 153 | bar, 154 | baz, 155 | toto, 156 | tutu, 157 | tata 158 | 159 | # Bad variable names regexes, separated by a comma. If names match any regex, 160 | # they will always be refused 161 | bad-names-rgxs= 162 | 163 | # Naming style matching correct class attribute names. 164 | class-attribute-naming-style=any 165 | 166 | # Regular expression matching correct class attribute names. Overrides class- 167 | # attribute-naming-style. 168 | #class-attribute-rgx= 169 | 170 | # Naming style matching correct class constant names. 171 | class-const-naming-style=UPPER_CASE 172 | 173 | # Regular expression matching correct class constant names. Overrides class- 174 | # const-naming-style. 175 | #class-const-rgx= 176 | 177 | # Naming style matching correct class names. 178 | class-naming-style=PascalCase 179 | 180 | # Regular expression matching correct class names. Overrides class-naming- 181 | # style. 182 | #class-rgx= 183 | 184 | # Naming style matching correct constant names. 185 | const-naming-style=UPPER_CASE 186 | 187 | # Regular expression matching correct constant names. Overrides const-naming- 188 | # style. 189 | #const-rgx= 190 | 191 | # Minimum line length for functions/classes that require docstrings, shorter 192 | # ones are exempt. 193 | docstring-min-length=-1 194 | 195 | # Naming style matching correct function names. 196 | function-naming-style=snake_case 197 | 198 | # Regular expression matching correct function names. Overrides function- 199 | # naming-style. 200 | #function-rgx= 201 | 202 | # Good variable names which should always be accepted, separated by a comma. 203 | good-names=i, 204 | j, 205 | k, 206 | ex, 207 | Run, 208 | _ 209 | 210 | # Good variable names regexes, separated by a comma. If names match any regex, 211 | # they will always be accepted 212 | good-names-rgxs= 213 | 214 | # Include a hint for the correct naming format with invalid-name. 215 | include-naming-hint=no 216 | 217 | # Naming style matching correct inline iteration names. 218 | inlinevar-naming-style=any 219 | 220 | # Regular expression matching correct inline iteration names. Overrides 221 | # inlinevar-naming-style. 222 | #inlinevar-rgx= 223 | 224 | # Naming style matching correct method names. 225 | method-naming-style=snake_case 226 | 227 | # Regular expression matching correct method names. Overrides method-naming- 228 | # style. 229 | #method-rgx= 230 | 231 | # Naming style matching correct module names. 232 | module-naming-style=snake_case 233 | 234 | # Regular expression matching correct module names. Overrides module-naming- 235 | # style. 236 | #module-rgx= 237 | 238 | # Colon-delimited sets of names that determine each other's naming style when 239 | # the name regexes allow several styles. 240 | name-group= 241 | 242 | # Regular expression which should only match function or class names that do 243 | # not require a docstring. 244 | no-docstring-rgx=^_ 245 | 246 | # List of decorators that produce properties, such as abc.abstractproperty. Add 247 | # to this list to register other decorators that produce valid properties. 248 | # These decorators are taken in consideration only for invalid-name. 249 | property-classes=abc.abstractproperty 250 | 251 | # Naming style matching correct variable names. 252 | variable-naming-style=snake_case 253 | 254 | # Regular expression matching correct variable names. Overrides variable- 255 | # naming-style. 256 | #variable-rgx= 257 | 258 | 259 | [FORMAT] 260 | 261 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 262 | expected-line-ending-format= 263 | 264 | # Regexp for a line that is allowed to be longer than the limit. 265 | ignore-long-lines=^\s*(# )??$ 266 | 267 | # Number of spaces of indent required inside a hanging or continued line. 268 | indent-after-paren=4 269 | 270 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 271 | # tab). 272 | indent-string=' ' 273 | 274 | # Maximum number of characters on a single line. 275 | max-line-length=100 276 | 277 | # Maximum number of lines in a module. 278 | max-module-lines=1000 279 | 280 | # Allow the body of a class to be on the same line as the declaration if body 281 | # contains single statement. 282 | single-line-class-stmt=no 283 | 284 | # Allow the body of an if to be on the same line as the test if there is no 285 | # else. 286 | single-line-if-stmt=no 287 | 288 | 289 | [LOGGING] 290 | 291 | # The type of string formatting that logging methods do. `old` means using % 292 | # formatting, `new` is for `{}` formatting. 293 | logging-format-style=old 294 | 295 | # Logging modules to check that the string format arguments are in logging 296 | # function parameter format. 297 | logging-modules=logging 298 | 299 | 300 | [MISCELLANEOUS] 301 | 302 | # List of note tags to take in consideration, separated by a comma. 303 | notes=FIXME, 304 | XXX, 305 | TODO 306 | 307 | # Regular expression of note tags to take in consideration. 308 | #notes-rgx= 309 | 310 | 311 | [SIMILARITIES] 312 | 313 | # Comments are removed from the similarity computation 314 | ignore-comments=yes 315 | 316 | # Docstrings are removed from the similarity computation 317 | ignore-docstrings=yes 318 | 319 | # Imports are removed from the similarity computation 320 | ignore-imports=no 321 | 322 | # Signatures are removed from the similarity computation 323 | ignore-signatures=no 324 | 325 | # Minimum lines number of a similarity. 326 | min-similarity-lines=4 327 | 328 | 329 | [SPELLING] 330 | 331 | # Limits count of emitted suggestions for spelling mistakes. 332 | max-spelling-suggestions=4 333 | 334 | # Spelling dictionary name. Available dictionaries: none. To make it work, 335 | # install the 'python-enchant' package. 336 | spelling-dict= 337 | 338 | # List of comma separated words that should be considered directives if they 339 | # appear and the beginning of a comment and should not be checked. 340 | spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: 341 | 342 | # List of comma separated words that should not be checked. 343 | spelling-ignore-words= 344 | 345 | # A path to a file that contains the private dictionary; one word per line. 346 | spelling-private-dict-file= 347 | 348 | # Tells whether to store unknown words to the private dictionary (see the 349 | # --spelling-private-dict-file option) instead of raising a message. 350 | spelling-store-unknown-words=no 351 | 352 | 353 | [STRING] 354 | 355 | # This flag controls whether inconsistent-quotes generates a warning when the 356 | # character used as a quote delimiter is used inconsistently within a module. 357 | check-quote-consistency=no 358 | 359 | # This flag controls whether the implicit-str-concat should generate a warning 360 | # on implicit string concatenation in sequences defined over several lines. 361 | check-str-concat-over-line-jumps=no 362 | 363 | 364 | [TYPECHECK] 365 | 366 | # List of decorators that produce context managers, such as 367 | # contextlib.contextmanager. Add to this list to register other decorators that 368 | # produce valid context managers. 369 | contextmanager-decorators=contextlib.contextmanager 370 | 371 | # List of members which are set dynamically and missed by pylint inference 372 | # system, and so shouldn't trigger E1101 when accessed. Python regular 373 | # expressions are accepted. 374 | generated-members= 375 | 376 | # Tells whether missing members accessed in mixin class should be ignored. A 377 | # class is considered mixin if its name matches the mixin-class-rgx option. 378 | ignore-mixin-members=yes 379 | 380 | # Tells whether to warn about missing members when the owner of the attribute 381 | # is inferred to be None. 382 | ignore-none=yes 383 | 384 | # This flag controls whether pylint should warn about no-member and similar 385 | # checks whenever an opaque object is returned when inferring. The inference 386 | # can return multiple potential results while evaluating a Python object, but 387 | # some branches might not be evaluated, which results in partial inference. In 388 | # that case, it might be useful to still emit no-member and other checks for 389 | # the rest of the inferred objects. 390 | ignore-on-opaque-inference=yes 391 | 392 | # List of class names for which member attributes should not be checked (useful 393 | # for classes with dynamically set attributes). This supports the use of 394 | # qualified names. 395 | ignored-classes=optparse.Values,thread._local,_thread._local 396 | 397 | # List of module names for which member attributes should not be checked 398 | # (useful for modules/projects where namespaces are manipulated during runtime 399 | # and thus existing member attributes cannot be deduced by static analysis). It 400 | # supports qualified module names, as well as Unix pattern matching. 401 | ignored-modules= 402 | 403 | # Show a hint with possible names when a member name was not found. The aspect 404 | # of finding the hint is based on edit distance. 405 | missing-member-hint=yes 406 | 407 | # The minimum edit distance a name should have in order to be considered a 408 | # similar match for a missing member name. 409 | missing-member-hint-distance=1 410 | 411 | # The total number of similar names that should be taken in consideration when 412 | # showing a hint for a missing member. 413 | missing-member-max-choices=1 414 | 415 | # Regex pattern to define which classes are considered mixins ignore-mixin- 416 | # members is set to 'yes' 417 | mixin-class-rgx=.*[Mm]ixin 418 | 419 | # List of decorators that change the signature of a decorated function. 420 | signature-mutators= 421 | 422 | 423 | [VARIABLES] 424 | 425 | # List of additional names supposed to be defined in builtins. Remember that 426 | # you should avoid defining new builtins when possible. 427 | additional-builtins= 428 | 429 | # Tells whether unused global variables should be treated as a violation. 430 | allow-global-unused-variables=yes 431 | 432 | # List of names allowed to shadow builtins 433 | allowed-redefined-builtins= 434 | 435 | # List of strings which can identify a callback function by name. A callback 436 | # name must start or end with one of those strings. 437 | callbacks=cb_, 438 | _cb 439 | 440 | # A regular expression matching the name of dummy variables (i.e. expected to 441 | # not be used). 442 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 443 | 444 | # Argument names that match this expression will be ignored. Default to name 445 | # with leading underscore. 446 | ignored-argument-names=_.*|^ignored_|^unused_ 447 | 448 | # Tells whether we should check for unused import in __init__ files. 449 | init-import=no 450 | 451 | # List of qualified module names which can have objects that can redefine 452 | # builtins. 453 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 454 | 455 | 456 | [CLASSES] 457 | 458 | # Warn about protected attribute access inside special methods 459 | check-protected-access-in-special-methods=no 460 | 461 | # List of method names used to declare (i.e. assign) instance attributes. 462 | defining-attr-methods=__init__, 463 | __new__, 464 | setUp, 465 | __post_init__ 466 | 467 | # List of member names, which should be excluded from the protected access 468 | # warning. 469 | exclude-protected=_asdict, 470 | _fields, 471 | _replace, 472 | _source, 473 | _make 474 | 475 | # List of valid names for the first argument in a class method. 476 | valid-classmethod-first-arg=cls 477 | 478 | # List of valid names for the first argument in a metaclass class method. 479 | valid-metaclass-classmethod-first-arg=cls 480 | 481 | 482 | [DESIGN] 483 | 484 | # List of regular expressions of class ancestor names to ignore when counting 485 | # public methods (see R0903) 486 | exclude-too-few-public-methods= 487 | 488 | # List of qualified class names to ignore when counting class parents (see 489 | # R0901) 490 | ignored-parents= 491 | 492 | # Maximum number of arguments for function / method. 493 | max-args=5 494 | 495 | # Maximum number of attributes for a class (see R0902). 496 | max-attributes=7 497 | 498 | # Maximum number of boolean expressions in an if statement (see R0916). 499 | max-bool-expr=5 500 | 501 | # Maximum number of branch for function / method body. 502 | max-branches=12 503 | 504 | # Maximum number of locals for function / method body. 505 | max-locals=15 506 | 507 | # Maximum number of parents for a class (see R0901). 508 | max-parents=7 509 | 510 | # Maximum number of public methods for a class (see R0904). 511 | max-public-methods=20 512 | 513 | # Maximum number of return / yield for function / method body. 514 | max-returns=6 515 | 516 | # Maximum number of statements in function / method body. 517 | max-statements=50 518 | 519 | # Minimum number of public methods for a class (see R0903). 520 | min-public-methods=1 521 | 522 | 523 | [IMPORTS] 524 | 525 | # List of modules that can be imported at any level, not just the top level 526 | # one. 527 | allow-any-import-level= 528 | 529 | # Allow wildcard imports from modules that define __all__. 530 | allow-wildcard-with-all=no 531 | 532 | # Analyse import fallback blocks. This can be used to support both Python 2 and 533 | # 3 compatible code, which means that the block might have code that exists 534 | # only in one or another interpreter, leading to false positives when analysed. 535 | analyse-fallback-blocks=no 536 | 537 | # Deprecated modules which should not be used, separated by a comma. 538 | deprecated-modules= 539 | 540 | # Output a graph (.gv or any supported image format) of external dependencies 541 | # to the given file (report RP0402 must not be disabled). 542 | ext-import-graph= 543 | 544 | # Output a graph (.gv or any supported image format) of all (i.e. internal and 545 | # external) dependencies to the given file (report RP0402 must not be 546 | # disabled). 547 | import-graph= 548 | 549 | # Output a graph (.gv or any supported image format) of internal dependencies 550 | # to the given file (report RP0402 must not be disabled). 551 | int-import-graph= 552 | 553 | # Force import order to recognize a module as part of the standard 554 | # compatibility libraries. 555 | known-standard-library= 556 | 557 | # Force import order to recognize a module as part of a third party library. 558 | known-third-party=enchant 559 | 560 | # Couples of modules and preferred modules, separated by a comma. 561 | preferred-modules= 562 | 563 | 564 | [EXCEPTIONS] 565 | 566 | # Exceptions that will emit a warning when being caught. Defaults to 567 | # "BaseException, Exception". 568 | overgeneral-exceptions=BaseException, 569 | Exception 570 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020, Johannes Schuck 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenCV-with-Python-Series 2 | 3 | Code repository for my tutorial series which covers beginner to advanced topics on OpenCV with Python. 😁 4 | 5 | I'm using Windows and Python 3.8. However, the code should be cross-platform and run without any problem on GNU/Linux and Mac OS. 6 | 7 | ## Running the Examples 8 | 9 | To run the examples yourself open a terminal and checkout the repo: 10 | 11 | $ git clone https://github.com/joschuck/OpenCV-with-Python-Series 12 | 13 | Next, install the required Python modules: 14 | 15 | $ cd OpenCV-with-Python-Series 16 | $ python -m pip install -r requirements.txt 17 | 18 | Once you have installed the modules run any example like this: 19 | 20 | $ python -m src.run 21 | 22 | ### 01 Thresholding 23 | 24 | This tutorial explains image thresholding (simple thresholding, adaptive thresholding and Otsu's method) by example through enhancement of document pictures. 25 | 26 | ![Adaptive Thresholding Demo](docs/img/01_thresholding_adaptive.gif) 27 | 28 | ## That's For Now 29 | 30 | Thanks for reading! 😊 31 | -------------------------------------------------------------------------------- /docs/img/01_thresholding_adaptive.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschuck/OpenCV-with-Python-Series/58bdfbd13dbe4dd0852724debafffa8caa0ddac1/docs/img/01_thresholding_adaptive.gif -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: OpenCV-with-Python-Series 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - python=3.8.18 6 | 7 | - pip: 8 | - -r requirements.txt 9 | -------------------------------------------------------------------------------- /examples/book_page_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschuck/OpenCV-with-Python-Series/58bdfbd13dbe4dd0852724debafffa8caa0ddac1/examples/book_page_1.jpg -------------------------------------------------------------------------------- /examples/book_page_1_cropped.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschuck/OpenCV-with-Python-Series/58bdfbd13dbe4dd0852724debafffa8caa0ddac1/examples/book_page_1_cropped.jpg -------------------------------------------------------------------------------- /examples/book_page_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschuck/OpenCV-with-Python-Series/58bdfbd13dbe4dd0852724debafffa8caa0ddac1/examples/book_page_2.jpg -------------------------------------------------------------------------------- /examples/book_page_2_cropped.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschuck/OpenCV-with-Python-Series/58bdfbd13dbe4dd0852724debafffa8caa0ddac1/examples/book_page_2_cropped.jpg -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pre-commit==3.5.0 2 | pylint==3.1.0 3 | black==24.4.2 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyside6==6.6.3.1 2 | opencv-python==4.9.0.80 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # pylint: skip-file 2 | 3 | from setuptools import setup, find_packages 4 | 5 | 6 | with open("README.md") as f: 7 | readme = f.read() 8 | 9 | with open("LICENSE") as f: 10 | license = f.read() 11 | 12 | setup( 13 | name="OpenCV-with-Python-Series", 14 | version="0.1.0", 15 | description="OpenCV with Python examples from my Youtube series: TODO", 16 | long_description=readme, 17 | author="Johannes Schuck", 18 | author_email="jojoschuck@gmail.com", 19 | url="https://github.com/joschuck/OpenCV-with-Python-Series", 20 | license=license, 21 | packages=find_packages(exclude=("tests", "docs")), 22 | ) 23 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschuck/OpenCV-with-Python-Series/58bdfbd13dbe4dd0852724debafffa8caa0ddac1/src/__init__.py -------------------------------------------------------------------------------- /src/fourier_transform/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschuck/OpenCV-with-Python-Series/58bdfbd13dbe4dd0852724debafffa8caa0ddac1/src/fourier_transform/__init__.py -------------------------------------------------------------------------------- /src/fourier_transform/fourier_transform.py: -------------------------------------------------------------------------------- 1 | """ 2 | Fourier transform demonstration. 3 | """ 4 | 5 | import cv2 6 | import numpy as np 7 | from PySide6.QtWidgets import ( 8 | QWidget, 9 | QPushButton, 10 | QLabel, 11 | QGridLayout, 12 | ) 13 | 14 | from src.util.dialog import open_image_dialog 15 | from src.util.image import to_q_pixmap 16 | from src.util.webcam import Webcam 17 | 18 | 19 | class FourierTransform(QWidget): 20 | """FourierTransform""" 21 | 22 | WIDTH = 480 23 | HEIGHT = 480 24 | 25 | def __init__(self): 26 | super().__init__() 27 | 28 | self.webcam = Webcam(0, self.WIDTH, self.HEIGHT) 29 | self.webcam.acquired_image_signal.connect(self.set_image) 30 | 31 | self.setWindowTitle("Fourier Transform") 32 | 33 | # Start webcam button 34 | self.start_webcam_button = QPushButton("Start Webcam") 35 | self.start_webcam_button.clicked.connect(self.start_webcam) 36 | 37 | # Open image button 38 | self.open_image_button = QPushButton("Open Image", self) 39 | self.open_image_button.clicked.connect(self.open_image) 40 | 41 | self.src_image_label = QLabel() 42 | self.dst_image_label = QLabel() 43 | 44 | # Create layout and add widgets 45 | layout = QGridLayout() 46 | layout.addWidget(self.start_webcam_button, 0, 0, 1, 2) 47 | layout.addWidget(self.open_image_button, 1, 0, 1, 2) 48 | layout.addWidget(self.src_image_label, 2, 0) 49 | layout.addWidget(self.dst_image_label, 2, 1) 50 | 51 | # Set dialog layout 52 | self.setLayout(layout) 53 | 54 | # pylint: disable=invalid-name 55 | def closeEvent(self, event): 56 | """Makes sure to stop the webcam before we close the widget.""" 57 | self.webcam.stop() 58 | event.accept() 59 | 60 | def start_webcam(self): 61 | """Starts the webcam.""" 62 | # Reassign functionality to pause the webcam on demand 63 | self.start_webcam_button.setText("Pause / Play") 64 | self.start_webcam_button.clicked.disconnect() 65 | self.start_webcam_button.clicked.connect(self.webcam.toggle) 66 | self.webcam.start() 67 | 68 | def open_image(self): 69 | """Shows a file dialog and displays the selected image.""" 70 | if self.webcam.isRunning(): 71 | self.webcam.stop() 72 | 73 | self.start_webcam_button.setText("Start Webcam") 74 | self.start_webcam_button.clicked.disconnect() 75 | self.start_webcam_button.clicked.connect(self.start_webcam) 76 | 77 | image = open_image_dialog() 78 | if image is not None: 79 | self.set_image(image) 80 | 81 | def set_image(self, image): 82 | """Updates the widget's image.""" 83 | self.src_image_label.setPixmap(to_q_pixmap(image)) 84 | 85 | # Compute phase, magnitude or spectrum 86 | gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) 87 | fft = np.fft.fft2(gray) 88 | fshift = np.fft.fftshift(fft) 89 | 90 | fshift = np.ascontiguousarray(fshift) 91 | 92 | magnitude_spectrum = np.log(np.abs(fshift)) 93 | magnitude_spectrum *= 255.0 / magnitude_spectrum.max() 94 | magnitude_spectrum = magnitude_spectrum.astype(np.uint8) 95 | self.dst_image_label.setPixmap(to_q_pixmap(magnitude_spectrum)) 96 | -------------------------------------------------------------------------------- /src/run.py: -------------------------------------------------------------------------------- 1 | """Launcher for all examples in the repository.""" 2 | 3 | import sys 4 | 5 | from PySide6.QtWidgets import ( 6 | QApplication, 7 | QVBoxLayout, 8 | QMainWindow, 9 | QListWidget, 10 | QListWidgetItem, 11 | QPushButton, 12 | QWidget, 13 | ) 14 | 15 | from src.fourier_transform.fourier_transform import FourierTransform 16 | from src.thresholding.adaptive_thresholding import AdaptiveThresholding 17 | from src.thresholding.binary_thresholding import BinaryThresholding 18 | from src.thresholding.otsus_thresholding import OtsusThresholding 19 | 20 | programs = { 21 | "Adaptive Thresholding": AdaptiveThresholding, 22 | "Binary Thresholding": BinaryThresholding, 23 | "Otsu's Thresholding": OtsusThresholding, 24 | "Fourier Transform": FourierTransform, 25 | } 26 | 27 | 28 | class ProgramSelector(QMainWindow): 29 | """Let's the user select an example.""" 30 | 31 | instances = [] 32 | 33 | def __init__(self): 34 | super().__init__() 35 | 36 | self.setWindowTitle("OpenCV with Python") 37 | 38 | layout = QVBoxLayout() 39 | self.program_list = QListWidget() 40 | 41 | for program in programs: 42 | self.program_list.addItem(QListWidgetItem(program)) 43 | 44 | start_button = QPushButton("Start") 45 | start_button.clicked.connect(self.start) 46 | 47 | layout.addWidget(self.program_list) 48 | layout.addWidget(start_button) 49 | 50 | # Set the central widget of the Window. 51 | central_widget = QWidget() 52 | central_widget.setLayout(layout) 53 | self.setCentralWidget(central_widget) 54 | 55 | def start(self): 56 | """Starts the selected example program.""" 57 | try: 58 | item = self.program_list.selectedItems()[0] 59 | instance = programs.get(item.text())() 60 | self.instances.append(instance) 61 | 62 | instance.show() 63 | except IndexError: 64 | return 65 | 66 | 67 | if __name__ == "__main__": 68 | app = QApplication(sys.argv) 69 | 70 | ex = ProgramSelector() 71 | 72 | ex.show() 73 | 74 | sys.exit(app.exec()) 75 | -------------------------------------------------------------------------------- /src/thresholding/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschuck/OpenCV-with-Python-Series/58bdfbd13dbe4dd0852724debafffa8caa0ddac1/src/thresholding/__init__.py -------------------------------------------------------------------------------- /src/thresholding/adaptive_thresholding.py: -------------------------------------------------------------------------------- 1 | """ 2 | Adaptive thresholding demonstration 3 | """ 4 | # pylint: disable=R0801 5 | 6 | import cv2 7 | import numpy as np 8 | from PySide6.QtCore import Qt 9 | from PySide6.QtGui import QImage, QPixmap 10 | from PySide6.QtWidgets import ( 11 | QWidget, 12 | QPushButton, 13 | QComboBox, 14 | QLabel, 15 | QSlider, 16 | QVBoxLayout, 17 | ) 18 | 19 | from src.util.dialog import open_image_dialog 20 | from src.util.math import round_up_to_odd 21 | 22 | 23 | class AdaptiveThresholding(QWidget): 24 | """AdaptiveThresholding""" 25 | 26 | titles = ["Original Image", "ADAPTIVE_THRESH_MEAN_C", "ADAPTIVE_THRESH_GAUSSIAN_C"] 27 | image: np.ndarray 28 | 29 | def __init__(self): 30 | super().__init__() 31 | 32 | self.setWindowTitle("Adaptive Thresholding") 33 | 34 | open_image_btn = QPushButton("Open Image", self) 35 | open_image_btn.clicked.connect(self.open_image) 36 | 37 | self.method_combobox = QComboBox() 38 | for title in self.titles: 39 | self.method_combobox.addItem(title) 40 | self.method_combobox.currentIndexChanged.connect(self.update_image) 41 | 42 | self.block_size_label = QLabel(f"Block Size: {self.block_size}") 43 | 44 | self.block_size_slider = QSlider() 45 | self.block_size_slider.setOrientation(Qt.Horizontal) 46 | self.block_size_slider.setTickPosition(QSlider.TicksBelow) 47 | self.block_size_slider.setMinimum(3) 48 | self.block_size_slider.setMaximum(255) 49 | self.block_size_slider.setTickInterval(2) 50 | self.block_size_slider.setSingleStep(2) 51 | self.block_size_slider.setValue(11) 52 | self.block_size_slider.valueChanged.connect(self.block_size) 53 | 54 | self.c_constant_label = QLabel(f"C constant: {self.c_constant}") 55 | 56 | self.c_constant_slider = QSlider() 57 | self.c_constant_slider.setOrientation(Qt.Horizontal) 58 | self.c_constant_slider.setTickPosition(QSlider.TicksBelow) 59 | self.c_constant_slider.setMinimum(0) 60 | self.c_constant_slider.setMaximum(100) 61 | self.c_constant_slider.setValue(2) 62 | self.c_constant_slider.valueChanged.connect(self.c_constant) 63 | 64 | self.image_label = QLabel() 65 | self.image = np.tile(np.arange(256, dtype=np.uint8).repeat(2), (512, 1)) 66 | q_img = QImage(self.image.data, 512, 512, 512, QImage.Format_Indexed8) 67 | self.image_label.setPixmap(QPixmap.fromImage(q_img)) 68 | 69 | # Create layout and add widgets 70 | layout = QVBoxLayout() 71 | layout.addWidget(open_image_btn) 72 | layout.addWidget(self.method_combobox) 73 | layout.addWidget(self.block_size_label) 74 | layout.addWidget(self.block_size_slider) 75 | layout.addWidget(self.c_constant_label) 76 | layout.addWidget(self.c_constant_slider) 77 | layout.addWidget(self.image_label) 78 | 79 | # Set dialog layout 80 | self.setLayout(layout) 81 | 82 | def update_image(self): 83 | """Updates the widget's image.""" 84 | method_idx = self.method_combobox.currentIndex() 85 | 86 | block_size = self.block_size_slider.value() 87 | c_constant = self.c_constant_slider.value() 88 | 89 | if method_idx == 1: 90 | image = cv2.adaptiveThreshold( 91 | self.image, 92 | 255, 93 | cv2.ADAPTIVE_THRESH_MEAN_C, 94 | cv2.THRESH_BINARY, 95 | block_size, 96 | c_constant, 97 | ) 98 | elif method_idx == 2: 99 | image = cv2.adaptiveThreshold( 100 | self.image, 101 | 255, 102 | cv2.ADAPTIVE_THRESH_GAUSSIAN_C, 103 | cv2.THRESH_BINARY, 104 | block_size, 105 | c_constant, 106 | ) 107 | else: 108 | image = self.image 109 | 110 | image_h, image_w = image.shape 111 | q_img = QImage(image.data, image_w, image_h, image_w, QImage.Format_Indexed8) 112 | self.image_label.setPixmap(QPixmap.fromImage(q_img)) 113 | 114 | def open_image(self): 115 | """Shows a file dialog and displays the selected image.""" 116 | image = open_image_dialog() 117 | if image is not None: 118 | self.image = image 119 | self.update_image() 120 | 121 | def block_size(self, block_size): 122 | """Sets the block size.""" 123 | block_size = round_up_to_odd(block_size) 124 | self.block_size_slider.setValue(block_size) 125 | self.block_size_label.setText(f"Block size: {self.block_size_slider.value()}") 126 | self.update_image() 127 | 128 | def c_constant(self, c_constant): 129 | """Sets the c constant.""" 130 | self.c_constant_slider.setValue(c_constant) 131 | self.c_constant_label.setText(f"C constant: {self.c_constant_slider.value()}") 132 | self.update_image() 133 | -------------------------------------------------------------------------------- /src/thresholding/binary_thresholding.py: -------------------------------------------------------------------------------- 1 | """ 2 | Binary thresholding demonstration. 3 | """ 4 | # pylint: disable=R0801 5 | 6 | import cv2 7 | import numpy as np 8 | from PySide6.QtCore import Qt 9 | from PySide6.QtGui import QImage, QPixmap 10 | from PySide6.QtWidgets import ( 11 | QWidget, 12 | QPushButton, 13 | QComboBox, 14 | QLabel, 15 | QSlider, 16 | QVBoxLayout, 17 | ) 18 | 19 | from src.util.dialog import open_image_dialog 20 | 21 | 22 | class BinaryThresholding(QWidget): 23 | """BinaryThresholding widget.""" 24 | 25 | titles = [ 26 | "Original Image", 27 | "THRESH_BINARY", 28 | "THRESH_BINARY_INV", 29 | "THRESH_TRUNC", 30 | "THRESH_TOZERO", 31 | "THRESH_TOZERO_INV", 32 | ] 33 | 34 | image: np.ndarray 35 | 36 | def __init__(self): 37 | super().__init__() 38 | 39 | self.setWindowTitle("OpenCV Binary Thresholding") 40 | 41 | open_image_btn = QPushButton("Open Image", self) 42 | open_image_btn.clicked.connect(self.open_image) 43 | 44 | self.method_combobox = QComboBox() 45 | for title in self.titles: 46 | self.method_combobox.addItem(title) 47 | self.method_combobox.currentIndexChanged.connect(self.update_image) 48 | 49 | self.threshold_label = QLabel("Threshold Value: 127") 50 | 51 | self.threshold_slider = QSlider() 52 | self.threshold_slider.setOrientation(Qt.Horizontal) 53 | self.threshold_slider.setTickPosition(QSlider.TicksBelow) 54 | self.threshold_slider.setTickInterval(10) 55 | self.threshold_slider.setMinimum(0) 56 | self.threshold_slider.setMaximum(255) 57 | self.threshold_slider.setValue(127) 58 | self.threshold_slider.valueChanged.connect(self.update_image) 59 | 60 | self.image_label = QLabel() 61 | self.image = np.tile(np.arange(256, dtype=np.uint8).repeat(2), (512, 1)) 62 | q_img = QImage(self.image.data, 512, 512, 512, QImage.Format_Indexed8) 63 | self.image_label.setPixmap(QPixmap.fromImage(q_img)) 64 | 65 | # Create layout and add widgets 66 | layout = QVBoxLayout() 67 | layout.addWidget(open_image_btn) 68 | layout.addWidget(self.method_combobox) 69 | layout.addWidget(self.threshold_label) 70 | layout.addWidget(self.threshold_slider) 71 | layout.addWidget(self.image_label) 72 | 73 | # Set dialog layout 74 | self.setLayout(layout) 75 | 76 | def open_image(self): 77 | """Shows a file dialog and displays the selected image.""" 78 | image = open_image_dialog() 79 | if image is not None: 80 | self.image = image 81 | self.update_image() 82 | 83 | def update_image(self): 84 | """Updates the widget's image.""" 85 | method_idx = self.method_combobox.currentIndex() 86 | threshold = self.threshold_slider.value() 87 | 88 | self.threshold_label.setText(f"Threshold value: {threshold}") 89 | if method_idx == 1: 90 | _, image = cv2.threshold(self.image, threshold, 255, cv2.THRESH_BINARY) 91 | elif method_idx == 2: 92 | _, image = cv2.threshold(self.image, threshold, 255, cv2.THRESH_BINARY_INV) 93 | elif method_idx == 3: 94 | _, image = cv2.threshold(self.image, threshold, 255, cv2.THRESH_TRUNC) 95 | elif method_idx == 4: 96 | _, image = cv2.threshold(self.image, threshold, 255, cv2.THRESH_TOZERO) 97 | elif method_idx == 5: 98 | _, image = cv2.threshold(self.image, threshold, 255, cv2.THRESH_TOZERO_INV) 99 | elif method_idx == 6: 100 | image = cv2.adaptiveThreshold( 101 | self.image, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 11, 2 102 | ) 103 | elif method_idx == 7: 104 | image = cv2.adaptiveThreshold( 105 | self.image, 106 | 255, 107 | cv2.ADAPTIVE_THRESH_GAUSSIAN_C, 108 | cv2.THRESH_BINARY, 109 | 11, 110 | 2, 111 | ) 112 | else: 113 | image = self.image 114 | 115 | image_h, image_w = image.shape 116 | q_img = QImage(image.data, image_w, image_h, image_w, QImage.Format_Indexed8) 117 | self.image_label.setPixmap(QPixmap.fromImage(q_img)) 118 | -------------------------------------------------------------------------------- /src/thresholding/otsus_thresholding.py: -------------------------------------------------------------------------------- 1 | """ 2 | Otsu's thresholding widget 3 | """ 4 | # pylint: disable=R0801 5 | 6 | import cv2 7 | import numpy as np 8 | 9 | from PySide6.QtGui import QImage, QPixmap 10 | from PySide6.QtWidgets import ( 11 | QWidget, 12 | QPushButton, 13 | QComboBox, 14 | QLabel, 15 | QVBoxLayout, 16 | ) 17 | 18 | from src.util.dialog import open_image_dialog 19 | 20 | 21 | class OtsusThresholding(QWidget): 22 | """OtsusThresholding""" 23 | 24 | image: np.ndarray 25 | 26 | titles = ["Original Image", "THRESH_BINARY + cv2.THRESH_OTSU"] 27 | 28 | def __init__(self): 29 | super().__init__() 30 | 31 | self.setWindowTitle("Otsu's Thresholding") 32 | 33 | open_image_btn = QPushButton("Open Image", self) 34 | open_image_btn.clicked.connect(self.open_image) 35 | 36 | self.method_combobox = QComboBox() 37 | for title in self.titles: 38 | self.method_combobox.addItem(title) 39 | self.method_combobox.currentIndexChanged.connect(self.update_image) 40 | 41 | self.threshold_label = QLabel("Threshold calculated: -") 42 | 43 | self.image_label = QLabel() 44 | 45 | self.image = np.tile(np.arange(256, dtype=np.uint8).repeat(2), (512, 1)) 46 | q_img = QImage(self.image.data, 512, 512, 512, QImage.Format_Indexed8) 47 | self.image_label.setPixmap(QPixmap.fromImage(q_img)) 48 | 49 | layout = QVBoxLayout() 50 | layout.addWidget(open_image_btn) 51 | layout.addWidget(self.method_combobox) 52 | layout.addWidget(self.threshold_label) 53 | layout.addWidget(self.image_label) 54 | 55 | self.setLayout(layout) 56 | 57 | def open_image(self): 58 | """Shows a file dialog and displays the selected image.""" 59 | image = open_image_dialog() 60 | if image is not None: 61 | self.image = image 62 | self.update_image() 63 | 64 | def update_image(self): 65 | """Updates the widget's image.""" 66 | method_idx = self.method_combobox.currentIndex() 67 | 68 | if method_idx == 0: 69 | ret, image = "-", self.image 70 | elif method_idx == 1: 71 | ret, image = cv2.threshold( 72 | self.image, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU 73 | ) 74 | else: 75 | return 76 | 77 | self.threshold_label.setText(f"Threshold calculated: {ret}") 78 | 79 | image_h, image_w = image.shape 80 | q_img = QImage(image.data, image_w, image_h, image_w, QImage.Format_Indexed8) 81 | self.image_label.setPixmap(QPixmap.fromImage(q_img)) 82 | -------------------------------------------------------------------------------- /src/util/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joschuck/OpenCV-with-Python-Series/58bdfbd13dbe4dd0852724debafffa8caa0ddac1/src/util/__init__.py -------------------------------------------------------------------------------- /src/util/dialog.py: -------------------------------------------------------------------------------- 1 | """ 2 | GUI dialog helpers 3 | """ 4 | from typing import Optional 5 | 6 | import cv2 7 | from PySide6.QtWidgets import QFileDialog, QWidget 8 | 9 | 10 | def open_image_dialog(parent: Optional[QWidget] = None): 11 | """Open image dialog.""" 12 | image_path, _ = QFileDialog.getOpenFileName( 13 | parent, "Load Image", filter="Image Files (*.tiff *.png *.jpeg *.jpg *.bmp)" 14 | ) 15 | 16 | if image_path: 17 | return cv2.imread(image_path, 0) 18 | 19 | return None 20 | -------------------------------------------------------------------------------- /src/util/image.py: -------------------------------------------------------------------------------- 1 | """ 2 | Qt/OpenCV adapter functions 3 | """ 4 | 5 | __author__ = "Johannes Schuck" 6 | __email__ = "johannes.schuck@gmail.com" 7 | 8 | __all__ = ["to_q_image", "to_q_pixmap"] 9 | 10 | from typing import Optional 11 | 12 | from PySide6.QtGui import QImage, QPixmap 13 | import numpy as np 14 | 15 | 16 | def to_q_image(image: np.ndarray) -> Optional[QImage]: 17 | """ 18 | Converts a OpenCV / NumPy array to a QImage. 19 | Expects the image to be 8 bit. 20 | Is able to convert Grayscale, BGR and ARGG images. 21 | 22 | Parameters 23 | ---------- 24 | image : np.ndarray 25 | Input Image 26 | 27 | Returns 28 | ------- 29 | QImage 30 | The converted image. If image is None, returns an empty QImage. 31 | """ 32 | if image is None: 33 | return QImage() 34 | 35 | if image.dtype != np.uint8: 36 | return None 37 | 38 | if len(image.shape) == 2: 39 | height, width = image.shape 40 | return QImage(image.data, width, height, width, QImage.Format_Indexed8) 41 | 42 | if len(image.shape) == 3: 43 | height, width, channels = image.shape 44 | if channels == 3: 45 | return QImage(image.data, width, height, width * 3, QImage.Format_BGR888) 46 | 47 | if channels == 4: 48 | return QImage(image.data, width, height, width * 3, QImage.Format_ARGB32) 49 | return None 50 | 51 | 52 | def to_q_pixmap(image: QPixmap) -> QPixmap: 53 | """ 54 | Converts a OpenCV / NumPy array to a QPixmap. 55 | Expects the image to be 8 bit. 56 | Is able to convert Grayscale, BGR and ARGG images. 57 | 58 | Parameters 59 | ---------- 60 | image : np.ndarray 61 | Input Image 62 | 63 | Returns 64 | ------- 65 | QPixmap 66 | The converted QPixmap. If image is None, returns an empty QPixmap. 67 | """ 68 | return QPixmap(to_q_image(image)) 69 | -------------------------------------------------------------------------------- /src/util/math.py: -------------------------------------------------------------------------------- 1 | """ 2 | math helpers 3 | """ 4 | 5 | import numpy as np 6 | 7 | 8 | def round_up_to_odd(number: float) -> int: 9 | """Round up to an odd number.""" 10 | return int(np.ceil(number) // 2 * 2 + 1) 11 | -------------------------------------------------------------------------------- /src/util/webcam.py: -------------------------------------------------------------------------------- 1 | """Webcam utility class.""" 2 | import cv2 3 | import numpy as np 4 | from PySide6.QtCore import QThread, Signal 5 | 6 | 7 | class Webcam(QThread): 8 | """OpenCV Webcam in QThread""" 9 | 10 | is_paused: bool = False 11 | acquired_image_signal: Signal = Signal(np.ndarray) 12 | 13 | def __init__(self, source, width, height, parent=None): 14 | QThread.__init__(self, parent) 15 | 16 | self.width = width 17 | self.height = height 18 | 19 | self.cap = cv2.VideoCapture(source) 20 | if self.cap is None or not self.cap.isOpened(): 21 | self.stop() 22 | 23 | self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, width) 24 | self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height) 25 | 26 | def run(self): 27 | """Main loop.""" 28 | while not self.isInterruptionRequested(): 29 | # Capture frame-by-frame 30 | ret, frame = self.cap.read() 31 | 32 | if self.is_paused: 33 | continue 34 | 35 | if ret: 36 | frame = cv2.resize(frame, (self.width, self.height)) 37 | self.acquired_image_signal.emit(frame) # send image to gui 38 | 39 | self.cap.release() 40 | 41 | def stop(self): 42 | """Stops the webcam.""" 43 | self.requestInterruption() 44 | self.wait(100) 45 | # self.quit() 46 | # self.terminate() 47 | 48 | def pause(self, pause): 49 | """Pauses the webcam.""" 50 | self.is_paused = pause 51 | 52 | def toggle(self): 53 | """Toggles play/pause.""" 54 | self.is_paused = not self.is_paused 55 | --------------------------------------------------------------------------------