first improvements in speedup

This commit is contained in:
Florian Förster 2026-02-26 17:58:39 +01:00
parent f39a8cc5f3
commit 31bc992834
13 changed files with 1026 additions and 25 deletions

View File

@ -1,26 +1,30 @@
# Repository Template for Python 3.11 Projects
## Tools for Project and Package Management
# KSG AOI image anomaly detection
[![pdm-managed](https://img.shields.io/endpoint?url=https%3A%2F%2Fcdn.jsdelivr.net%2Fgh%2Fpdm-project%2F.github%2Fbadge.json)](https://pdm-project.org)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
Python projects are managed with **PDM** ([link to GitHub Source](https://github.com/pdm-project/pdm)), a PEP-compliant project and dependency management tool.
The applicable settings are contained within the PyProject-TOML file. In order to use a repo which was created with this template is to tell PDM which Python interpreter to use and then to install the whole project into the created virtual environment. If the interpreter is not available you will need to install it via PDM.
## General
### Lib structure by Susanne
- configs:
- ``config.py``: reduced version of original config to work as a small test example
- ``config_for_test.py``: special parameters for test environment, minimal working example
- main program:
- ``main.py``: loads GUI and background worker to observe changes in folder directory
- GUI:
- ``window_manager.py``
- ``gui_ai_on_off.py``
- Logic:
- ``monitor.py``: worker logic to observe changes in the relevant folders
- function ``monitor_folder``
- ``preparation.py``: main logic to process data ``class Preparation``, namely:
- copy (backup) of found data to this app's saving directory (``config.py -- STORING_PATH``)
- method ``copy_ngt_and_checkimg``
- colourisation of images (only the first) (yellow)
- method ``change_image_to_yellow``
- fuse the different RGB layers to one image and save it
- method ``create_rgb_images_and_patches``
```console
pdm use 3.11.11 # example of a given version
pdm install
```
This installs all mandatory development dependencies such as:
- Ruff (formatting and linting)
- pytest (unittest framework)
- coverage.py (measuring test coverage)
- pytest-cov (integration of coverage into pytest)
- pytest-xdist (allows to execute the tests on multiple CPU cores)
- bump-my-version (CLI tool to manage version bumping)
- Nox (Python runner, e.g. to run test suite on multiple Python versions)
- pdoc (to auto-generate documentation from docstrings)
- Jupyterlab and widgets (to perform fast prototyping and enable exploratory data analysis)

318
pdm.lock generated
View File

@ -5,10 +5,10 @@
groups = ["default", "dev", "lint", "nb", "tests"]
strategy = ["inherit_metadata"]
lock_version = "4.5.0"
content_hash = "sha256:3a107981dc4305f031f87c89e3a57a6bb823954d397a52d074fef1c72ac639d0"
content_hash = "sha256:326ca1095302e816f56644c4ba0929ea12b930348375c98eba139701e0388de6"
[[metadata.targets]]
requires_python = ">=3.11"
requires_python = ">=3.11,<3.15"
[[package]]
name = "annotated-types"
@ -271,7 +271,7 @@ name = "cffi"
version = "2.0.0"
requires_python = ">=3.9"
summary = "Foreign Function Interface for Python calling C code."
groups = ["nb"]
groups = ["default", "nb"]
dependencies = [
"pycparser; implementation_name != \"PyPy\"",
]
@ -1542,6 +1542,108 @@ files = [
{file = "nox-2026.2.9.tar.gz", hash = "sha256:1bc8a202ee8cd69be7aaada63b2a7019126899a06fc930a7aee75585bf8ee41b"},
]
[[package]]
name = "numpy"
version = "2.4.2"
requires_python = ">=3.11"
summary = "Fundamental package for array computing in Python"
groups = ["default"]
files = [
{file = "numpy-2.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7e88598032542bd49af7c4747541422884219056c268823ef6e5e89851c8825"},
{file = "numpy-2.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7edc794af8b36ca37ef5fcb5e0d128c7e0595c7b96a2318d1badb6fcd8ee86b1"},
{file = "numpy-2.4.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:6e9f61981ace1360e42737e2bae58b27bf28a1b27e781721047d84bd754d32e7"},
{file = "numpy-2.4.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cb7bbb88aa74908950d979eeaa24dbdf1a865e3c7e45ff0121d8f70387b55f73"},
{file = "numpy-2.4.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f069069931240b3fc703f1e23df63443dbd6390614c8c44a87d96cd0ec81eb1"},
{file = "numpy-2.4.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c02ef4401a506fb60b411467ad501e1429a3487abca4664871d9ae0b46c8ba32"},
{file = "numpy-2.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2653de5c24910e49c2b106499803124dde62a5a1fe0eedeaecf4309a5f639390"},
{file = "numpy-2.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1ae241bbfc6ae276f94a170b14785e561cb5e7f626b6688cf076af4110887413"},
{file = "numpy-2.4.2-cp311-cp311-win32.whl", hash = "sha256:df1b10187212b198dd45fa943d8985a3c8cf854aed4923796e0e019e113a1bda"},
{file = "numpy-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:b9c618d56a29c9cb1c4da979e9899be7578d2e0b3c24d52079c166324c9e8695"},
{file = "numpy-2.4.2-cp311-cp311-win_arm64.whl", hash = "sha256:47c5a6ed21d9452b10227e5e8a0e1c22979811cad7dcc19d8e3e2fb8fa03f1a3"},
{file = "numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a"},
{file = "numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1"},
{file = "numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e"},
{file = "numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27"},
{file = "numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548"},
{file = "numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f"},
{file = "numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460"},
{file = "numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba"},
{file = "numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f"},
{file = "numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85"},
{file = "numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa"},
{file = "numpy-2.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25f2059807faea4b077a2b6837391b5d830864b3543627f381821c646f31a63c"},
{file = "numpy-2.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd3a7a9f5847d2fb8c2c6d1c862fa109c31a9abeca1a3c2bd5a64572955b2979"},
{file = "numpy-2.4.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8e4549f8a3c6d13d55041925e912bfd834285ef1dd64d6bc7d542583355e2e98"},
{file = "numpy-2.4.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:aea4f66ff44dfddf8c2cffd66ba6538c5ec67d389285292fe428cb2c738c8aef"},
{file = "numpy-2.4.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3cd545784805de05aafe1dde61752ea49a359ccba9760c1e5d1c88a93bbf2b7"},
{file = "numpy-2.4.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0d9b7c93578baafcbc5f0b83eaf17b79d345c6f36917ba0c67f45226911d499"},
{file = "numpy-2.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f74f0f7779cc7ae07d1810aab8ac6b1464c3eafb9e283a40da7309d5e6e48fbb"},
{file = "numpy-2.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7ac672d699bf36275c035e16b65539931347d68b70667d28984c9fb34e07fa7"},
{file = "numpy-2.4.2-cp313-cp313-win32.whl", hash = "sha256:8e9afaeb0beff068b4d9cd20d322ba0ee1cecfb0b08db145e4ab4dd44a6b5110"},
{file = "numpy-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:7df2de1e4fba69a51c06c28f5a3de36731eb9639feb8e1cf7e4a7b0daf4cf622"},
{file = "numpy-2.4.2-cp313-cp313-win_arm64.whl", hash = "sha256:0fece1d1f0a89c16b03442eae5c56dc0be0c7883b5d388e0c03f53019a4bfd71"},
{file = "numpy-2.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5633c0da313330fd20c484c78cdd3f9b175b55e1a766c4a174230c6b70ad8262"},
{file = "numpy-2.4.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d9f64d786b3b1dd742c946c42d15b07497ed14af1a1f3ce840cce27daa0ce913"},
{file = "numpy-2.4.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:b21041e8cb6a1eb5312dd1d2f80a94d91efffb7a06b70597d44f1bd2dfc315ab"},
{file = "numpy-2.4.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00ab83c56211a1d7c07c25e3217ea6695e50a3e2f255053686b081dc0b091a82"},
{file = "numpy-2.4.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fb882da679409066b4603579619341c6d6898fc83a8995199d5249f986e8e8f"},
{file = "numpy-2.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66cb9422236317f9d44b67b4d18f44efe6e9c7f8794ac0462978513359461554"},
{file = "numpy-2.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0f01dcf33e73d80bd8dc0f20a71303abbafa26a19e23f6b68d1aa9990af90257"},
{file = "numpy-2.4.2-cp313-cp313t-win32.whl", hash = "sha256:52b913ec40ff7ae845687b0b34d8d93b60cb66dcee06996dd5c99f2fc9328657"},
{file = "numpy-2.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:5eea80d908b2c1f91486eb95b3fb6fab187e569ec9752ab7d9333d2e66bf2d6b"},
{file = "numpy-2.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fd49860271d52127d61197bb50b64f58454e9f578cb4b2c001a6de8b1f50b0b1"},
{file = "numpy-2.4.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:444be170853f1f9d528428eceb55f12918e4fda5d8805480f36a002f1415e09b"},
{file = "numpy-2.4.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d1240d50adff70c2a88217698ca844723068533f3f5c5fa6ee2e3220e3bdb000"},
{file = "numpy-2.4.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:7cdde6de52fb6664b00b056341265441192d1291c130e99183ec0d4b110ff8b1"},
{file = "numpy-2.4.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:cda077c2e5b780200b6b3e09d0b42205a3d1c68f30c6dceb90401c13bff8fe74"},
{file = "numpy-2.4.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d30291931c915b2ab5717c2974bb95ee891a1cf22ebc16a8006bd59cd210d40a"},
{file = "numpy-2.4.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bba37bc29d4d85761deed3954a1bc62be7cf462b9510b51d367b769a8c8df325"},
{file = "numpy-2.4.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b2f0073ed0868db1dcd86e052d37279eef185b9c8db5bf61f30f46adac63c909"},
{file = "numpy-2.4.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7f54844851cdb630ceb623dcec4db3240d1ac13d4990532446761baede94996a"},
{file = "numpy-2.4.2-cp314-cp314-win32.whl", hash = "sha256:12e26134a0331d8dbd9351620f037ec470b7c75929cb8a1537f6bfe411152a1a"},
{file = "numpy-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:068cdb2d0d644cdb45670810894f6a0600797a69c05f1ac478e8d31670b8ee75"},
{file = "numpy-2.4.2-cp314-cp314-win_arm64.whl", hash = "sha256:6ed0be1ee58eef41231a5c943d7d1375f093142702d5723ca2eb07db9b934b05"},
{file = "numpy-2.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:98f16a80e917003a12c0580f97b5f875853ebc33e2eaa4bccfc8201ac6869308"},
{file = "numpy-2.4.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:20abd069b9cda45874498b245c8015b18ace6de8546bf50dfa8cea1696ed06ef"},
{file = "numpy-2.4.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e98c97502435b53741540a5717a6749ac2ada901056c7db951d33e11c885cc7d"},
{file = "numpy-2.4.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da6cad4e82cb893db4b69105c604d805e0c3ce11501a55b5e9f9083b47d2ffe8"},
{file = "numpy-2.4.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e4424677ce4b47fe73c8b5556d876571f7c6945d264201180db2dc34f676ab5"},
{file = "numpy-2.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2b8f157c8a6f20eb657e240f8985cc135598b2b46985c5bccbde7616dc9c6b1e"},
{file = "numpy-2.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5daf6f3914a733336dab21a05cdec343144600e964d2fcdabaac0c0269874b2a"},
{file = "numpy-2.4.2-cp314-cp314t-win32.whl", hash = "sha256:8c50dd1fc8826f5b26a5ee4d77ca55d88a895f4e4819c7ecc2a9f5905047a443"},
{file = "numpy-2.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fcf92bee92742edd401ba41135185866f7026c502617f422eb432cfeca4fe236"},
{file = "numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181"},
{file = "numpy-2.4.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:89f7268c009bc492f506abd6f5265defa7cb3f7487dc21d357c3d290add45082"},
{file = "numpy-2.4.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6dee3bb76aa4009d5a912180bf5b2de012532998d094acee25d9cb8dee3e44a"},
{file = "numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:cd2bd2bbed13e213d6b55dc1d035a4f91748a7d3edc9480c13898b0353708920"},
{file = "numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:cf28c0c1d4c4bf00f509fa7eb02c58d7caf221b50b467bcb0d9bbf1584d5c821"},
{file = "numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e04ae107ac591763a47398bb45b568fc38f02dbc4aa44c063f67a131f99346cb"},
{file = "numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:602f65afdef699cda27ec0b9224ae5dc43e328f4c24c689deaf77133dbee74d0"},
{file = "numpy-2.4.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be71bf1edb48ebbbf7f6337b5bfd2f895d1902f6335a5830b20141fc126ffba0"},
{file = "numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae"},
]
[[package]]
name = "opencv-python"
version = "4.13.0.92"
requires_python = ">=3.6"
summary = "Wrapper package for OpenCV python bindings."
groups = ["default"]
dependencies = [
"numpy<2.0; python_version < \"3.9\"",
"numpy>=2; python_version >= \"3.9\"",
]
files = [
{file = "opencv_python-4.13.0.92-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:caf60c071ec391ba51ed00a4a920f996d0b64e3e46068aac1f646b5de0326a19"},
{file = "opencv_python-4.13.0.92-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:5868a8c028a0b37561579bfb8ac1875babdc69546d236249fff296a8c010ccf9"},
{file = "opencv_python-4.13.0.92-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bc2596e68f972ca452d80f444bc404e08807d021fbba40df26b61b18e01838a"},
{file = "opencv_python-4.13.0.92-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:402033cddf9d294693094de5ef532339f14ce821da3ad7df7c9f6e8316da32cf"},
{file = "opencv_python-4.13.0.92-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:bccaabf9eb7f897ca61880ce2869dcd9b25b72129c28478e7f2a5e8dee945616"},
{file = "opencv_python-4.13.0.92-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:620d602b8f7d8b8dab5f4b99c6eb353e78d3fb8b0f53db1bd258bb1aa001c1d5"},
{file = "opencv_python-4.13.0.92-cp37-abi3-win32.whl", hash = "sha256:372fe164a3148ac1ca51e5f3ad0541a4a276452273f503441d718fab9c5e5f59"},
{file = "opencv_python-4.13.0.92-cp37-abi3-win_amd64.whl", hash = "sha256:423d934c9fafb91aad38edf26efb46da91ffbc05f3f59c4b0c72e699720706f5"},
]
[[package]]
name = "overrides"
version = "7.7.0"
@ -1619,6 +1721,95 @@ files = [
{file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"},
]
[[package]]
name = "pillow"
version = "12.1.1"
requires_python = ">=3.10"
summary = "Python Imaging Library (fork)"
groups = ["default"]
files = [
{file = "pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32"},
{file = "pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38"},
{file = "pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5"},
{file = "pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090"},
{file = "pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af"},
{file = "pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b"},
{file = "pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5"},
{file = "pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d"},
{file = "pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c"},
{file = "pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563"},
{file = "pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80"},
{file = "pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052"},
{file = "pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984"},
{file = "pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79"},
{file = "pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293"},
{file = "pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397"},
{file = "pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0"},
{file = "pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3"},
{file = "pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35"},
{file = "pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a"},
{file = "pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6"},
{file = "pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523"},
{file = "pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e"},
{file = "pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9"},
{file = "pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6"},
{file = "pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60"},
{file = "pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2"},
{file = "pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850"},
{file = "pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289"},
{file = "pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e"},
{file = "pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717"},
{file = "pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a"},
{file = "pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029"},
{file = "pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b"},
{file = "pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1"},
{file = "pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a"},
{file = "pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da"},
{file = "pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc"},
{file = "pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c"},
{file = "pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8"},
{file = "pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20"},
{file = "pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13"},
{file = "pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf"},
{file = "pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524"},
{file = "pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986"},
{file = "pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c"},
{file = "pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3"},
{file = "pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af"},
{file = "pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f"},
{file = "pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642"},
{file = "pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd"},
{file = "pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202"},
{file = "pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f"},
{file = "pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f"},
{file = "pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f"},
{file = "pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e"},
{file = "pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0"},
{file = "pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb"},
{file = "pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f"},
{file = "pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15"},
{file = "pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f"},
{file = "pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8"},
{file = "pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9"},
{file = "pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60"},
{file = "pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7"},
{file = "pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f"},
{file = "pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586"},
{file = "pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce"},
{file = "pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8"},
{file = "pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36"},
{file = "pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b"},
{file = "pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334"},
{file = "pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f"},
{file = "pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9"},
{file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e"},
{file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9"},
{file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3"},
{file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735"},
{file = "pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e"},
{file = "pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4"},
]
[[package]]
name = "platformdirs"
version = "4.9.2"
@ -1722,7 +1913,7 @@ name = "pycparser"
version = "3.0"
requires_python = ">=3.10"
summary = "C parser in Python"
groups = ["nb"]
groups = ["default", "nb"]
marker = "implementation_name != \"PyPy\""
files = [
{file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"},
@ -1864,6 +2055,60 @@ files = [
{file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"},
]
[[package]]
name = "pyside6"
version = "6.10.2"
requires_python = "<3.15,>=3.9"
summary = "Python bindings for the Qt cross-platform application and UI framework"
groups = ["default"]
dependencies = [
"PySide6-Addons==6.10.2",
"PySide6-Essentials==6.10.2",
"shiboken6==6.10.2",
]
files = [
{file = "pyside6-6.10.2-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:4b084293caa7845d0064aaf6af258e0f7caae03a14a33537d0a552131afddaf0"},
{file = "pyside6-6.10.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:1b89ce8558d4b4f35b85bff1db90d680912e4d3ce9e79ff804d6fef1d1a151ef"},
{file = "pyside6-6.10.2-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:0439f5e9b10ebe6177981bac9e219096ec970ac6ec215bef055279802ba50601"},
{file = "pyside6-6.10.2-cp39-abi3-win_amd64.whl", hash = "sha256:032bad6b18a17fcbf4dddd0397f49b07f8aae7f1a45b7e4de7037bf7fd6e0edf"},
{file = "pyside6-6.10.2-cp39-abi3-win_arm64.whl", hash = "sha256:65a59ad0bc92525639e3268d590948ce07a80ee97b55e7a9200db41d493cac31"},
]
[[package]]
name = "pyside6-addons"
version = "6.10.2"
requires_python = "<3.15,>=3.9"
summary = "Python bindings for the Qt cross-platform application and UI framework (Addons)"
groups = ["default"]
dependencies = [
"PySide6-Essentials==6.10.2",
"shiboken6==6.10.2",
]
files = [
{file = "pyside6_addons-6.10.2-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:0de7d0c9535e17d5e3b634b61314a1867f3b0f6d35c3d7cdc99efc353192faff"},
{file = "pyside6_addons-6.10.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:030a851163b51dbf0063be59e9ddb6a9e760bde89a28e461ccc81a224d286eaf"},
{file = "pyside6_addons-6.10.2-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:fcee0373e3fd7b98f014094e5e37b4a39e4de7c5a47c13f654a7d557d4a426ad"},
{file = "pyside6_addons-6.10.2-cp39-abi3-win_amd64.whl", hash = "sha256:c20150068525a17494f3b6576c5d61c417cf9a5870659e29f5ebd83cd20a78ea"},
{file = "pyside6_addons-6.10.2-cp39-abi3-win_arm64.whl", hash = "sha256:3d18db739b46946ba7b722d8ad4cc2097135033aa6ea57076e64d591e6a345f3"},
]
[[package]]
name = "pyside6-essentials"
version = "6.10.2"
requires_python = "<3.15,>=3.9"
summary = "Python bindings for the Qt cross-platform application and UI framework (Essentials)"
groups = ["default"]
dependencies = [
"shiboken6==6.10.2",
]
files = [
{file = "pyside6_essentials-6.10.2-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:1dee2cb9803ff135f881dadeb5c0edcef793d1ec4f8a9140a1348cecb71074e1"},
{file = "pyside6_essentials-6.10.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:660aea45bfa36f1e06f799b934c2a7df963bd31abc5083e8bb8a5bfaef45686b"},
{file = "pyside6_essentials-6.10.2-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:c2b028e4c6f8047a02c31f373408e23b4eedfd405f56c6aba8d0525c29472835"},
{file = "pyside6_essentials-6.10.2-cp39-abi3-win_amd64.whl", hash = "sha256:0741018c2b6395038cad4c41775cfae3f13a409e87995ac9f7d89e5b1fb6b22a"},
{file = "pyside6_essentials-6.10.2-cp39-abi3-win_arm64.whl", hash = "sha256:db5f4913648bb6afddb8b347edae151ee2378f12bceb03c8b2515a530a4b38d9"},
]
[[package]]
name = "pytest"
version = "9.0.2"
@ -1970,6 +2215,57 @@ files = [
{file = "python_json_logger-4.0.0.tar.gz", hash = "sha256:f58e68eb46e1faed27e0f574a55a0455eecd7b8a5b88b85a784519ba3cff047f"},
]
[[package]]
name = "pyvips"
version = "3.1.1"
requires_python = ">=3.7"
summary = "binding for the libvips image processing library"
groups = ["default"]
dependencies = [
"cffi>=1.0.0",
]
files = [
{file = "pyvips-3.1.1.tar.gz", hash = "sha256:84fe744d023b1084ac2516bb17064cacd41c7f8aabf8e524dd383534941b9301"},
]
[[package]]
name = "pyvips-binary"
version = "8.18.0"
requires_python = ">=3.7"
summary = "Binary distribution of libvips and dependencies for use with pyvips"
groups = ["default"]
dependencies = [
"cffi>=1.0.0",
]
files = [
{file = "pyvips_binary-8.18.0-cp37-abi3-macosx_10_15_x86_64.whl", hash = "sha256:6ff72bd6c60bb6cf75b7827083b64e275a15a7d862628b5716998350c17426c8"},
{file = "pyvips_binary-8.18.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:a570dbf76bb620efc9745d82d6493da504d56b21b035ccd876e358a0c182e018"},
{file = "pyvips_binary-8.18.0-cp37-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dad3012233b7b12f48180f2a407a50854e44654f37168fa8d42583d9e4f15882"},
{file = "pyvips_binary-8.18.0-cp37-abi3-manylinux_2_26_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0906be336b8f775e2d33dfe61ffc480ff83c91c08d5eeff904c27c2c5164ff3a"},
{file = "pyvips_binary-8.18.0-cp37-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4ddd4d344f758483d1630a9a08f201ab95162599acc6a8e6c62bb1563e94fe0"},
{file = "pyvips_binary-8.18.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:076fb0affa2901af0fee90c728ded6eed2c72f00356af9895fa7a1fb6c9a2288"},
{file = "pyvips_binary-8.18.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:659ef1e4af04b3472e7762a95caa1038fdeea530807c84a23a0f4c706af0338f"},
{file = "pyvips_binary-8.18.0-cp37-abi3-win32.whl", hash = "sha256:fd331bcd75bff8651d73d09687d55ac8fb9014baa5682b770a4ea0fbcedf5f97"},
{file = "pyvips_binary-8.18.0-cp37-abi3-win_amd64.whl", hash = "sha256:a67d73683f70c21bf2c336b6d5ddc2bd54ec36db72cc54ab63cb48bc2373feac"},
{file = "pyvips_binary-8.18.0-cp37-abi3-win_arm64.whl", hash = "sha256:0c1f9af910866bc8c2d55182e7a6e8684a828ee4d6084dd814e88e2ee9ec4be3"},
{file = "pyvips_binary-8.18.0.tar.gz", hash = "sha256:2f9e509de6d0cf04ea9b429ff0649130a9cf04de8a4f0887d2bcb72e3973225a"},
]
[[package]]
name = "pyvips"
version = "3.1.1"
extras = ["binary"]
requires_python = ">=3.7"
summary = "binding for the libvips image processing library"
groups = ["default"]
dependencies = [
"pyvips-binary",
"pyvips==3.1.1",
]
files = [
{file = "pyvips-3.1.1.tar.gz", hash = "sha256:84fe744d023b1084ac2516bb17064cacd41c7f8aabf8e524dd383534941b9301"},
]
[[package]]
name = "pywinpty"
version = "3.0.3"
@ -2387,6 +2683,20 @@ files = [
{file = "setuptools-82.0.0.tar.gz", hash = "sha256:22e0a2d69474c6ae4feb01951cb69d515ed23728cf96d05513d36e42b62b37cb"},
]
[[package]]
name = "shiboken6"
version = "6.10.2"
requires_python = "<3.15,>=3.9"
summary = "Python/C++ bindings helper module"
groups = ["default"]
files = [
{file = "shiboken6-6.10.2-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:3bd4e94e9a3c8c1fa8362fd752d399ef39265d5264e4e37bae61cdaa2a00c8c7"},
{file = "shiboken6-6.10.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ace0790032d9cb0adda644b94ee28d59410180d9773643bb6cf8438c361987ad"},
{file = "shiboken6-6.10.2-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:f74d3ed1f92658077d0630c39e694eb043aeb1d830a5d275176c45d07147427f"},
{file = "shiboken6-6.10.2-cp39-abi3-win_amd64.whl", hash = "sha256:10f3c8c5e1b8bee779346f21c10dbc14cff068f0b0b4e62420c82a6bf36ac2e7"},
{file = "shiboken6-6.10.2-cp39-abi3-win_arm64.whl", hash = "sha256:20c671645d70835af212ee05df60361d734c5305edb2746e9875c6a31283f963"},
]
[[package]]
name = "six"
version = "1.17.0"

View File

@ -6,8 +6,8 @@ authors = [
{name = "Susanne Franke", email = "s.franke@d-opt.de"},
{name = "Florian Förster", email = "f.foerster@d-opt.com"},
]
dependencies = []
requires-python = ">=3.11"
dependencies = ["PySide6>=6.10.2", "numpy>=2.4.2", "pillow>=12.1.1", "opencv-python>=4.13.0.92", "pyvips[binary]>=3.1.1"]
requires-python = "<3.15,>=3.11"
readme = "README.md"
license = {text = "LicenseRef-Proprietary"}

View File

@ -0,0 +1,32 @@
import shutil
from pathlib import Path
from KSG_anomaly_detection.config_for_test import PATH
BASE_PATH = Path(PATH)
# delete "Daten"
def recreate_folder(folder_name: str) -> Path:
p_data = BASE_PATH / folder_name
if p_data.exists():
shutil.rmtree(p_data)
p_data.mkdir(parents=True)
return p_data
def main() -> None:
_ = recreate_folder("Daten")
_ = recreate_folder("KI")
p_data = recreate_folder(
"./Verifizierdaten_1/20260225/614706_helles Entek/614706_helles Entek[3136761]_1"
)
p_orig_data = (
BASE_PATH / "_Originaldaten/614706_helles Entek/614706_helles Entek[3136761]_1"
)
assert p_orig_data.exists(), "original data not existing"
shutil.copytree(p_orig_data, p_data, dirs_exist_ok=True)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,27 @@
import cProfile
import pstats
from KSG_anomaly_detection import _prepare_env
from KSG_anomaly_detection.monitor import monitor_folder_simple
profiler = cProfile.Profile()
PROFILE = True
USE_NEW_IMPL = True
def main() -> None:
_prepare_env.main()
if PROFILE:
profiler.enable()
monitor_folder_simple(use_new=USE_NEW_IMPL)
profiler.disable()
stats = pstats.Stats(profiler).sort_stats("cumtime")
stats.print_stats(15)
else:
monitor_folder_simple(use_new=USE_NEW_IMPL)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,9 @@
from time import perf_counter
from KSG_anomaly_detection.monitor import monitor_folder_simple
if __name__ == "__main__":
t1 = perf_counter()
monitor_folder_simple()
t2 = perf_counter()
print(f"Elasped time: {(t2 - t1)} s")

View File

@ -0,0 +1,17 @@
import config_for_test
# Monitoring
START_DATE = "20260225"
MONITOR_PATH = rf"{config_for_test.PATH}"
AGE_THRESHOLD = (
10 # how long nothing new is allowed to have been created within the folder (in seconds)
)
# Processing: turned depending on AI on/off, we decide whether the folder is used for anomaly detection or not
V_1 = "Verifizierdaten_1" # Name des Ordners der Daten von Verifizierstation 1
CURRENT_PATH_RGB = rf"{config_for_test.PATH}\Daten" # dort werden die Ordner mit den RGB-AOI-Bildern abgelegt
# Fileserver: Datensicherung
STORING_PATH = rf"{config_for_test.PATH}\KI"

View File

@ -0,0 +1,9 @@
# BITTE ANPASSEN
# Pfad zu dem Ordner, in dem die Ordner 'KI' und 'Verifizierdaten_1' liegen
PATH = r"B:\projects\KSG\Ordnerstruktur"
# Pfad zu den einzelnen Päckchen, die untersucht werden sollen
FOLDER_LIST = [
r"B:\projects\KSG\Ordnerstruktur\Verifizierdaten_1\20260225\614706_helles Entek\614706_helles Entek[3136761]_1"
]

View File

@ -0,0 +1,87 @@
from PySide6.QtCore import Qt
from PySide6.QtGui import QFont
from PySide6.QtWidgets import (
QHBoxLayout,
QLabel,
QPushButton,
QSizePolicy,
QVBoxLayout,
QWidget,
)
class ToggleGUI(QWidget):
def __init__(self, title, background_colour):
super().__init__()
self.title = title
self.bg_colour = background_colour
self.setWindowTitle("KI-Algorithmus")
self.setMinimumSize(300, 200)
# Label
self.label = QLabel(self.title) # Visper 1 or Visper 2
self.label.setAlignment(Qt.AlignCenter)
font = QFont("Arial", 42, QFont.Bold)
self.label.setFont(font)
self.label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
# Buttons for turning KI on/off
self.btn_on = QPushButton("KI eingeschaltet")
self.btn_off = QPushButton("KI ausgeschaltet")
for btn in (self.btn_on, self.btn_off):
btn.setCheckable(True)
btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.set_button_style(btn, "lightgrey")
self.btn_on.clicked.connect(lambda: self.handle_toggle(self.btn_on))
self.btn_off.clicked.connect(lambda: self.handle_toggle(self.btn_off))
# Layouts for positioning
btn_layout = QHBoxLayout()
btn_layout.setSpacing(20)
btn_layout.addWidget(self.btn_on)
btn_layout.addWidget(self.btn_off)
main_layout = QVBoxLayout()
main_layout.setSpacing(20)
main_layout.setContentsMargins(40, 40, 40, 40)
main_layout.addWidget(self.label)
main_layout.addLayout(btn_layout)
main_layout.addStretch(1)
self.setLayout(main_layout)
# default state: ON
self.handle_toggle(self.btn_on)
# background colour to distinguish the different GUIs from eacht other
self.setStyleSheet(f"background-color: {self.bg_colour};")
def set_button_style(self, button, color):
button.setStyleSheet(f"""
QPushButton {{
background-color: {color};
padding: 20px;
font-size: 24px;
border: none;
border-radius: 10px;
}}
""")
def handle_toggle(self, clicked_button):
if clicked_button == self.btn_on:
self.btn_on.setChecked(True)
self.btn_off.setChecked(False)
self.set_button_style(self.btn_on, "lightgreen")
self.set_button_style(self.btn_off, "lightgrey")
else:
self.btn_off.setChecked(True)
self.btn_on.setChecked(False)
self.set_button_style(self.btn_off, "lightcoral")
self.set_button_style(self.btn_on, "lightgrey")
def is_enabled(self):
# we return True if 'KI eingeschaltet' is active
return self.btn_on.isChecked()

View File

@ -0,0 +1,22 @@
import sys
import threading
from PySide6.QtWidgets import QApplication
from KSG_anomaly_detection.monitor import (
monitor_folder, # Check für neue Ordner und Auslösen des KI-Algorithmus
)
from KSG_anomaly_detection.window_manager import WindowManager # zum Erzeugen der GUI
if __name__ == "__main__":
app = QApplication(sys.argv)
manager = WindowManager()
manager._create_v1()
# manager._create_v2()
# manager._create_stats()
monitor_thread = threading.Thread(target=monitor_folder, args=(manager,), daemon=True)
monitor_thread.start()
sys.exit(app.exec())

View File

@ -0,0 +1,179 @@
import os
import re
import time
import traceback
from pathlib import Path
from KSG_anomaly_detection import config, config_for_test
from KSG_anomaly_detection.preparation import Preparation
from KSG_anomaly_detection.window_manager import WindowManager
# Identifikation aller Unterordner jeweils in Verifizierstation_1 und Verifizierstation_2, eindeutige Lose
def get_third_level_subfolders(path):
seen_basenames = set()
result = set()
pattern = re.compile(r"^(.*)_\d+$")
for level1 in os.listdir(path):
level1_path = os.path.join(path, level1)
if not os.path.isdir(level1_path):
continue
if not level1 >= config.START_DATE:
continue
for level2 in os.listdir(level1_path):
level2_path = os.path.join(level1_path, level2)
if not os.path.isdir(level2_path):
continue
for level3 in os.listdir(level2_path):
level3_path = os.path.join(level2_path, level3)
if not os.path.isdir(level3_path):
continue
match = pattern.match(level3)
if match:
base_name = level3
if base_name not in seen_basenames:
seen_basenames.add(base_name)
base_path = os.path.join(level2_path, base_name)
result.add(base_path)
return result
# zur Identifikation, ob wir uns in Verifizierstation_1 und Verifizierstation_2 befinden
def get_first_level_name(folder_path):
# returns the name of the immediate parent folder (e.g., V1 or V2)
return Path(folder_path).parts[-4]
def is_entire_folder_unchanged(folder_path, threshold_seconds=300):
now = time.time()
for root, dirs, files in os.walk(folder_path):
for entry in dirs + files:
try:
entry_path = os.path.join(root, entry)
mtime = os.path.getmtime(entry_path)
if now - mtime < threshold_seconds:
return False
except FileNotFoundError:
continue
return True
def are_all_matching_folders_unchanged(base_path, name_substring, threshold_seconds):
for root, dirs, _ in os.walk(base_path):
for d in dirs:
if name_substring in d:
folder_path = os.path.join(root, d)
if not is_entire_folder_unchanged(folder_path, threshold_seconds):
return False
return True
# Hauptfunktion: Check auf neue Ordner und ggf. Auslösen des KI-Algorithmus
def monitor_folder(manager: WindowManager):
print("starting thread...")
while True:
for folder in config_for_test.FOLDER_LIST:
try:
if are_all_matching_folders_unchanged(
os.path.dirname(folder),
os.path.basename(folder),
threshold_seconds=config.AGE_THRESHOLD,
):
# prüfen, ob Verifizierstation_1 oder Verifizierstation_2
first_level = get_first_level_name(folder)
# ein zu Verifizierstation_1 zugehöriger Ordner und KI-Algorithmus soll durchgeführt werden
if first_level == config.V_1 and manager.get_checkbox_state_v1():
# Vorbereitung
preparation = Preparation(folder)
# Aufgabe 2: NGT und check_img von Originalordner in KI kopieren
# Rückgabewert: Ordner Fileserver/KI auf dem Fileserver, wo dann die Heatmaps abgelegt werden
file_ki_folder, result = preparation.copy_ngt_and_checkimg()
if result: # d. h. Fehler ist aufgetreten
continue # zu nächstem neuen folder springen
# Aufgabe 3: check_img im Originalordner anpassen (d. h. gelbe Farbe: work in progress)
preparation.change_image_to_yellow()
# Aufgabe 4: AOI-Bilder in RGB überführen und zwischenspeichern
# wir erhalten hier den Speicherort sowie ggf. Fehlermeldungen zurück
current_folder, result = preparation.create_rgb_images_and_patches()
print("finished routine")
if result is not None:
print(result)
continue
except Exception as e:
tb = traceback.extract_tb(e.__traceback__)
no = tb[-1].lineno
print(e, no)
time.sleep(60)
def monitor_folder_simple(use_new: bool):
print("starting thread...")
for folder in config_for_test.FOLDER_LIST:
# try:
if are_all_matching_folders_unchanged(
os.path.dirname(folder),
os.path.basename(folder),
threshold_seconds=config.AGE_THRESHOLD,
):
# prüfen, ob Verifizierstation_1 oder Verifizierstation_2
first_level = get_first_level_name(folder)
# ein zu Verifizierstation_1 zugehöriger Ordner und KI-Algorithmus soll durchgeführt werden
if first_level == config.V_1:
# Vorbereitung
preparation = Preparation(folder)
# Aufgabe 2: NGT und check_img von Originalordner in KI kopieren
# Rückgabewert: Ordner Fileserver/KI auf dem Fileserver, wo dann die Heatmaps abgelegt werden
print("'copy_ngt_and_checkimg'...")
file_ki_folder, result = preparation.copy_ngt_and_checkimg()
if result: # d. h. Fehler ist aufgetreten
continue # zu nächstem neuen folder springen
# Aufgabe 3: check_img im Originalordner anpassen (d. h. gelbe Farbe: work in progress)
print("'change_image_to_yellow'...")
if use_new:
preparation.change_image_to_yellow_new()
else:
preparation.change_image_to_yellow()
# Aufgabe 4: AOI-Bilder in RGB überführen und zwischenspeichern
# wir erhalten hier den Speicherort sowie ggf. Fehlermeldungen zurück
print("'create_rgb_images_and_patches'...")
if use_new:
current_folder, result = preparation.create_rgb_images_and_patches_new()
else:
current_folder, result = preparation.create_rgb_images_and_patches()
print("finished routine")
if result is not None:
print(result)
continue
# except Exception as e:
# tb = traceback.extract_tb(e.__traceback__)
# no = tb[-1].lineno
# print(e, no)
# time.sleep(60)

View File

@ -0,0 +1,283 @@
import os
import re
import traceback
from pathlib import Path
from pprint import pprint
from shutil import copytree
from typing import Literal, cast
import pyvips
from PIL import Image
from pyvips import Image as vipsImage
from KSG_anomaly_detection import config
Image.MAX_IMAGE_PIXELS = None
COLOUR_ASSIGNMENT = {"R": [255, 0, 0], "G": [0, 255, 0], "B": [0, 0, 0]}
class Preparation:
def __init__(self, folder):
self.folder_path = (
folder # der aktuelle Ordner mit neuen AOI-Bilddateien auf dem KI-Rechner
)
self.visper = Path(self.folder_path).parts[-4]
self.original_data_path = (
Path(config.STORING_PATH)
/ self.visper
/ Path(self.folder_path).parent.parent.name
/ Path(self.folder_path).parent.name
) # Pfad zu Fileserver/KI/...
print(f"[{self.visper}] {Path(self.folder_path).name} Vorbereitung gestartet...")
# ------------------------------------- Zweite Aufgabe: ngt und check_img kopieren ------------------------------------
def copy_ngt_and_checkimg(self):
try:
# extract last level name because we need to copy all folders containing this name
target_name = os.path.basename(self.folder_path)
base_path = Path(self.folder_path).parent
folders_to_copy = []
with os.scandir(base_path) as entries:
for entry in entries:
if entry.is_dir() and target_name in entry.name:
folders_to_copy.append(entry.path)
for src in folders_to_copy:
# TODO duplicate -> "self.original_data_path"
dst = (
Path(config.STORING_PATH)
/ self.visper
/ Path(self.folder_path).parent.parent.name
/ Path(self.folder_path).parent.name
/ Path(src).name
)
copytree(src, dst, dirs_exist_ok=True)
return Path(config.STORING_PATH) / self.visper / Path(
self.folder_path
).parent.parent / Path(self.folder_path).parent, None
except FileExistsError:
return (
None,
f"Fehlermeldung: Ordner {Path(self.folder_path).parts[-1]} existiert bereits.",
)
except Exception as e:
tb = traceback.extract_tb(e.__traceback__)
no = tb[-1].lineno
return None, f"Fehlermeldung: {e}, {no}"
# --------------------------- Dritte Aufgabe: check_img auf dem Fileserver auf Gelb ("Work in Progress") ändern --------------------------
def change_image_to_yellow(self):
# first we define for R, G and B which coour has to be adapted
colour_assignment = {"R": (255, 0, 0), "G": (0, 255, 0), "B": (0, 0, 0)}
base_path = Path(self.folder_path).parent
# iterate over all 'checkimg' folders recursively
for img_folder in base_path.rglob("checkimg"):
if not img_folder.is_dir():
continue
# iterate over image files inside this 'checkimg' folder and only change first ones (because these are the ones to be shown at the Verifizierstation)
for image_file in img_folder.glob("????1_1*"):
# from the file name, we extract whether it is the R, G or B part of the RGB image
# i.e. these are still the Einzelkanalfarbbilder
colour_channel = image_file.stem[0]
# change image
####################### ist eigentlich bekannt über DB #######################
with Image.open(image_file) as img:
size = img.size
####################### ist eigentlich bekannt über DB #######################
new_img = Image.new("RGB", size, colour_assignment[colour_channel])
# save the modified image
new_img.save(image_file)
def pyvips_blank_image(
self,
size: tuple[int, int],
channel: Literal["R", "G", "B"],
) -> vipsImage:
img = pyvips.Image.black(size[0], size[1], bands=3) + COLOUR_ASSIGNMENT[channel] # type: ignore
img = img.cast("uchar") # type: ignore
img = img.copy(interpretation="srgb")
return img
def change_image_to_yellow_new(self):
base_path = Path(self.folder_path).parent
# iterate over all 'checkimg' folders recursively
# !! check needed
# sizes should be the same for the same camera, only obtain once
# dict: {camera_number: (width, height)}
cam_img_sizes: dict[str, tuple[int, int]] = {}
cam_rgb_pictures: dict[str, dict[str, vipsImage]] = {
cam_no: {} for cam_no in ("1", "2")
}
for image_file in base_path.rglob("checkimg/????1_1*"):
# print("processing image file: ", image_file)
# from the file name, we extract whether it is the R, G or B part of the RGB image
# i.e. these are still the Einzelkanalfarbbilder
colour_channel = image_file.stem[0]
camera_num = str(image_file.parents[1])[-1]
if camera_num not in cam_img_sizes:
img_read = pyvips.Image.new_from_file(image_file, access="random")
size = img_read.width, img_read.height
cam_img_sizes[camera_num] = size
if (
camera_num not in cam_rgb_pictures
or colour_channel not in cam_rgb_pictures[camera_num]
):
blank_img = self.pyvips_blank_image(size, colour_channel)
cam_rgb_pictures[camera_num][colour_channel] = blank_img
img = cam_rgb_pictures[camera_num][colour_channel]
img_file_temp = (image_file.parent / (image_file.stem + "_temp")).with_suffix(
image_file.suffix
)
img.write_to_file(img_file_temp)
os.replace(img_file_temp, image_file)
# --------------------------- Vierte Aufgabe: AOI-Einzelkanalfarbbilder zur RGB-Bildern zusammenfügen --------------------------
# within the current folder to be inspected (i.e., self.folder_path), there are subfolders for the different A/B sides
# we have to extract the relative path starting from self.folder_path
def extract_folder_path_within_one_AOI_folder(
self, current_folder_to_inspect, checkimg_folders, folder_type
):
for folder_to_inspect in checkimg_folders:
if folder_to_inspect.is_dir() and (
Path(folder_to_inspect).parts[-2] == Path(current_folder_to_inspect).parts[-2]
):
relative_path = current_folder_to_inspect.relative_to(self.original_data_path)
relative_path = Path(*relative_path.parts[1:])
return relative_path
def create_rgb_images_and_patches(self):
# in the folders of interest, we iterate over all images and search for the three that belong together
# (because in advance we do not know how many there are)
pattern = re.compile(r"R_NG(\d+)_(\d+)\.jpg$")
# create folder name in our temp folder "Backup" and store it
# therefore, first extract the name of the current folder from the whole path
folder_name = Path(self.folder_path).name
new_folder_path = Path(config.CURRENT_PATH_RGB) / folder_name
try:
new_folder_path.mkdir(parents=True, exist_ok=False)
except FileExistsError:
return (
None,
f"Fehlermeldung: Ordner {Path(self.folder_path).parts[-1]} existiert bereits.",
)
except Exception as e:
return None, f"Fehlermeldung: {e}"
# find all checkimg folders within the folder
checkimg_folders = [
p for p in self.original_data_path.rglob("checkimg") if p.is_dir()
]
# iterate through all 'checkimg' folders recursively
for current_folder_to_inspect in checkimg_folders:
# identify the path starting from self.folder_path until the checkimg folder
relative_path = self.extract_folder_path_within_one_AOI_folder(
current_folder_to_inspect, checkimg_folders, "checkimg"
)
save_path_rgb = new_folder_path / relative_path
save_path_rgb.mkdir(parents=True, exist_ok=True)
for file_path in current_folder_to_inspect.glob("R_NG*_*.jpg"):
# find match according to pattern defined at the very beginning
match = pattern.match(file_path.name)
if not match:
continue
num1, num2 = match.groups()
# find all three images belonging together
r_path = file_path
g_path = current_folder_to_inspect / f"G_NG{num1}_{num2}.jpg"
b_path = current_folder_to_inspect / f"B_NG{num1}_{num2}.jpg"
# open all three images and combine them to RGB
with (
Image.open(r_path) as r,
Image.open(g_path) as g,
Image.open(b_path) as b,
):
r = r.convert("L")
g = g.convert("L")
b = b.convert("L")
rgb_image = Image.merge("RGB", (r, g, b))
filename = f"RGB_NG{num1}_{num2}.png"
rgb_image.save(save_path_rgb / filename)
return "folder_name", None
def create_rgb_images_and_patches_new(self):
# in the folders of interest, we iterate over all images and search for the three that belong together
# (because in advance we do not know how many there are)
pattern = re.compile(r"R_NG(\d+)_(\d+)\.jpg$")
# create folder name in our temp folder "Backup" and store it
# therefore, first extract the name of the current folder from the whole path
folder_name = Path(self.folder_path).name
new_folder_path = Path(config.CURRENT_PATH_RGB) / folder_name
try:
new_folder_path.mkdir(parents=True, exist_ok=False)
except FileExistsError:
return (
None,
f"Fehlermeldung: Ordner {Path(self.folder_path).parts[-1]} existiert bereits.",
)
except Exception as e:
return None, f"Fehlermeldung: {e}"
# find all checkimg folders within the folder
checkimg_folders = [
p for p in self.original_data_path.rglob("checkimg") if p.is_dir()
]
# iterate through all 'checkimg' folders recursively
for current_folder_to_inspect in checkimg_folders:
# identify the path starting from self.folder_path until the checkimg folder
relative_path = self.extract_folder_path_within_one_AOI_folder(
current_folder_to_inspect, checkimg_folders, "checkimg"
)
save_path_rgb = new_folder_path / relative_path
save_path_rgb.mkdir(parents=True, exist_ok=True)
for file_path in current_folder_to_inspect.glob("R_NG*_*.jpg"):
# find match according to pattern defined at the very beginning
match = pattern.match(file_path.name)
if not match:
continue
num1, num2 = match.groups()
# find all three images belonging together
r_path = file_path
g_path = current_folder_to_inspect / f"G_NG{num1}_{num2}.jpg"
b_path = current_folder_to_inspect / f"B_NG{num1}_{num2}.jpg"
# open all three images and combine them to RGB
r = pyvips.Image.new_from_file(r_path, access="sequential")
g = pyvips.Image.new_from_file(g_path, access="sequential")
b = pyvips.Image.new_from_file(b_path, access="sequential")
rgb_image = r.bandjoin([g, b])
rgb_image = rgb_image.copy(interpretation="srgb")
filename = f"RGB_NG{num1}_{num2}.png"
rgb_image.write_to_file(save_path_rgb / filename)
return "folder_name", None

View File

@ -0,0 +1,22 @@
from PySide6.QtCore import QObject, Signal, Slot
from KSG_anomaly_detection.gui_ai_on_off import ToggleGUI
class WindowManager(QObject):
recreate_v1 = Signal()
def __init__(self):
super().__init__()
self.v1 = None
self.recreate_v1.connect(self._create_v1)
@Slot()
def _create_v1(self):
self.v1 = ToggleGUI("Visper 1", "#e6e6fa")
self.v1.move(100, 100)
self.v1.show()
def get_checkbox_state_v1(self):
return self.v1 and self.v1.is_enabled()