Merge pull request 'working demo code with significant speed-up' (#1) from demo into main

Reviewed-on: #1
This commit is contained in:
Florian Förster 2026-03-02 14:49:18 +00:00
commit 18fdab60e6
14 changed files with 1344 additions and 27 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)

322
pdm.lock generated
View File

@ -2,13 +2,13 @@
# It is not intended for manual editing.
[metadata]
groups = ["default", "dev", "lint", "nb", "tests"]
groups = ["default", "dev", "lint", "nb", "open-cv", "tests"]
strategy = ["inherit_metadata"]
lock_version = "4.5.0"
content_hash = "sha256:3a107981dc4305f031f87c89e3a57a6bb823954d397a52d074fef1c72ac639d0"
content_hash = "sha256:3c47abf04bea7dfd195f350d89b59416b8492aaaf54257badfb8b4814b20e996"
[[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", "open-cv"]
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 = ["open-cv"]
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"
@ -1671,7 +1862,7 @@ name = "psutil"
version = "7.2.2"
requires_python = ">=3.6"
summary = "Cross-platform lib for process and system monitoring."
groups = ["nb"]
groups = ["default", "nb"]
files = [
{file = "psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b"},
{file = "psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea"},
@ -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,11 +6,15 @@ 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", "pyvips[binary]>=3.1.1", "psutil>=7.2.2"]
requires-python = "<3.15,>=3.11"
readme = "README.md"
license = {text = "LicenseRef-Proprietary"}
[project.optional-dependencies]
open-cv = [
"opencv-python>=4.13.0.92",
]
[build-system]
requires = ["pdm-backend"]
build-backend = "pdm.backend"

View File

@ -0,0 +1,63 @@
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:
paths_src: list[Path] = []
paths_dst: list[Path] = []
_ = recreate_folder("Daten")
_ = recreate_folder("KI")
# packages
p_orig_data = (
BASE_PATH / "_Originaldaten/614706_helles Entek/614706_helles Entek[3136761]_1"
)
assert p_orig_data.exists(), "original data not existing"
paths_src.append(p_orig_data)
p_data = recreate_folder(
"Verifizierdaten_1/20260225/614706_helles Entek/614706_helles Entek[3136761]_1"
)
paths_dst.append(p_data)
p_orig_data = (
BASE_PATH / "_Originaldaten/614706_helles Entek/614706_helles Entek[3136761]_2"
)
assert p_orig_data.exists(), "original data not existing"
paths_src.append(p_orig_data)
p_data = recreate_folder(
"Verifizierdaten_1/20260225/614706_helles Entek/614706_helles Entek[3136761]_2"
)
paths_dst.append(p_data)
p_orig_data = (
BASE_PATH / "_Originaldaten/614706_helles Entek/614706_helles Entek[3136761]_3"
)
assert p_orig_data.exists(), "original data not existing"
paths_src.append(p_orig_data)
p_data = recreate_folder(
"Verifizierdaten_1/20260225/614706_helles Entek/614706_helles Entek[3136761]_3"
)
paths_dst.append(p_data)
for src, dst in zip(paths_src, paths_dst):
shutil.copytree(src, dst, dirs_exist_ok=True)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,43 @@
import cProfile
import pstats
import time
from KSG_anomaly_detection import _prepare_env, delegator
from KSG_anomaly_detection.monitor import monitor_folder_simple
profiler = cProfile.Profile()
PROFILE = True
USE_NEW_IMPL = False
USE_MP = False
ONLY_PREPARE = False
def main() -> None:
_prepare_env.main()
if ONLY_PREPARE:
return
mp_pool = delegator.MPPool()
try:
t1 = time.perf_counter()
if PROFILE:
profiler.enable()
monitor_folder_simple(mp_pool=mp_pool, use_new=USE_NEW_IMPL, use_mp=USE_MP)
profiler.disable()
stats = pstats.Stats(profiler).sort_stats("cumtime")
ENTRIES_TO_SHOW = 40 if USE_MP else 20
stats.print_stats(ENTRIES_TO_SHOW)
else:
monitor_folder_simple(mp_pool=mp_pool, use_new=USE_NEW_IMPL, use_mp=USE_MP)
t2 = time.perf_counter()
finally:
mp_pool.close()
print(f"Elapsed time: {t2 - t1} s")
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,11 @@
# 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",
r"B:\projects\KSG\Ordnerstruktur\Verifizierdaten_1\20260225\614706_helles Entek\614706_helles Entek[3136761]_2",
r"B:\projects\KSG\Ordnerstruktur\Verifizierdaten_1\20260225\614706_helles Entek\614706_helles Entek[3136761]_3",
]

View File

@ -0,0 +1,77 @@
from __future__ import annotations
import math
import multiprocessing as mp
from collections.abc import Callable, Collection, Iterable
from typing import Any, TypeVar
import psutil
T = TypeVar("T")
D = TypeVar("D")
class MPPool:
def __init__(
self,
) -> None:
self.num_workers = psutil.cpu_count(logical=False) or 4
print("Set number of workers to: ", self.num_workers)
self.pool = mp.Pool(processes=self.num_workers)
def enrich_data_funcargs(
self,
data: Iterable[T],
arg: D,
) -> list[tuple[T, D]]:
return [(entry, arg) for entry in data]
def get_chunksize(
self,
data: Collection[Any],
) -> int:
chunk_size = max(1, math.ceil(len(data) / self.num_workers))
return chunk_size
def chunk_data(
self,
data: list[T],
chunk_size: int | None = None,
) -> list[list[T]]:
if chunk_size is None:
chunk_size = max(1, len(data) // self.num_workers)
chunks = [data[i : i + chunk_size] for i in range(0, len(data), chunk_size)]
chunks_assigned = chunks[: self.num_workers]
if len(chunks) - self.num_workers > 0:
open_chunks = chunks[self.num_workers :]
open_entries = (entry for chunk in open_chunks for entry in chunk)
for idx, entry in enumerate(open_entries):
chunks_assigned[idx].append(entry)
return chunks_assigned
def map(
self,
func: Callable[[Any], None],
chunks: Iterable[Any],
) -> None:
# assumes pre-batched data with "chunk_data"
_ = self.pool.map(func, chunks, chunksize=1)
def starmap(
self,
func: Callable[[Any], None],
chunks: Iterable[tuple[Any, ...]],
) -> None:
# assumes pre-batched data with "chunk_data"
_ = self.pool.starmap(func, chunks, chunksize=1)
def close(self) -> None:
self.pool.close()
self.pool.join()
def terminate(self) -> None:
self.pool.terminate()

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,202 @@
import os
import re
import sys
import time
import traceback
from pathlib import Path
from KSG_anomaly_detection import config, config_for_test, delegator
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(mp_pool: delegator.MPPool, use_new: bool, use_mp: bool):
print("starting procedure...")
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'...")
if use_new:
file_ki_folder, result = preparation.copy_ngt_and_checkimg_new()
else:
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)
SKIP_NEXT = False
if not SKIP_NEXT:
print("'change_image_to_yellow'...")
if use_new:
preparation.change_image_to_yellow_new()
else:
preparation.change_image_to_yellow()
# sys.exit(0)
# Aufgabe 4: AOI-Bilder in RGB überführen und zwischenspeichern
# wir erhalten hier den Speicherort sowie ggf. Fehlermeldungen zurück
SKIP_NEXT = False
if not use_mp and not SKIP_NEXT:
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()
SKIP_NEXT = False
if use_mp and not SKIP_NEXT:
print("'create_rgb_images_and_patches' multiprocessing...")
if use_new:
current_folder, result = (
preparation.create_rgb_images_and_patches_new2(mp_pool)
)
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,446 @@
import multiprocessing
import os
import re
import sys
import traceback
from collections.abc import Iterable
from pathlib import Path
from pprint import pprint
from shutil import copytree
from typing import Literal, TypeAlias, cast
import pyvips
from PIL import Image
from pyvips import Image as vipsImage
from KSG_anomaly_detection import config, delegator
Image.MAX_IMAGE_PIXELS = None
COLOUR_ASSIGNMENT = {"R": [255, 0, 0], "G": [0, 255, 0], "B": [0, 0, 0]}
RE_CHANNEL_MAPPING = re.compile(r"R_NG(\d+)_(\d+)\.jpg$")
class Preparation:
def __init__(self, folder):
# der aktuelle Ordner mit neuen AOI-Bilddateien auf dem KI-Rechner
self.folder_path = folder
self.path = Path(self.folder_path)
# ?? verify existence --> fail early?
self.visper = self.path.parts[-4]
# self.original_data_path = (
# Path(config.STORING_PATH)
# / self.visper
# / Path(self.folder_path).parents[1].name
# / Path(self.folder_path).parent.name
# ) # Pfad zu Fileserver/KI/...
# the original path's last component is the product type name instead of the package
self.original_data_base_path = Path(config.STORING_PATH).joinpath(
*self.path.parts[-4:-1]
)
self.original_data_path = self.original_data_base_path / self.path.name
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:
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}"
def copy_ngt_and_checkimg_new(self):
try:
# extract last level name because we need to copy all folders containing this name
# target_name = os.path.basename(self.folder_path)
# target_name = self.path.name
# base_path = self.path.parent
# ?? Warum kopieren wir nicht gleich den ganzen Ordner? --> Das Ergebnis der unten stehenden
# ?? Operation ist eine Liste, in der nur der Name des Ordners (Päckchen-Ebene) steht, die also eigentlich
# ?? immer nur einen Eintrag enthält --> self.path
# Einzige Ausnahme: mehr als 10 Einträge/Päckchen, dann enthalten die Päckchen 10-19 auch den Namen des
# ersten Päckchens
# Annahme hier: Es soll nur die Daten des entsprechenden Päckchens kopiert werden
# 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:
# # 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)
src = self.path
# dst = self.original_data_base_path / self.path.name
copytree(src, self.original_data_path, dirs_exist_ok=True)
# ?? Soll hier der Pfad mit den kopierten Originaldaten zurückgegeben werden?
# Das ist aktuell nicht der Fall, da die letzten Pfadkomponenten als absolute Pfade das
# Ergebnis komplett überschreiben
# Zudem wird der Basispfad (also Produktebene, nicht Päckchen zurückgegeben)!
# return Path(config.STORING_PATH) / self.visper / Path(
# self.folder_path
# ).parent.parent / Path(self.folder_path).parent, None
return self.original_data_base_path, 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):
# ?? Hier sind wir nicht mehr auf Ebene des Päckchens, sondern im übergeordneteten Bereich. Ist das
# ?? richtig so? Beim Kopieren waren wir nur auf Päckchen-Ebene unterwegs.
# Wenn jetzt neue Instanzen dieser Klasse mit anderen Päckchen desselben Basisordners erstellt werden, werden
# die Berechnungen an dieser Stelle immer erneut durchgeführt, auch für die anderen Päckchen, egal ob bereits
# durchgeführt oder durch Nutzer abgewählt
# Annahme hier: Es sollen nur Dateien des aktuellen Päckchens bearbeitet werden.
base_path = self.path
# 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:
print(
f"\n-------------------------------\n{folder_to_inspect=}\n,{current_folder_to_inspect=}\n"
)
check_1 = Path(folder_to_inspect).parts[-2]
check_2 = Path(current_folder_to_inspect).parts[-2]
print(f"{check_1=},\n{check_2=}\n------------------------------------")
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_base_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_base_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)
# 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 = self.path.name
# print("folder name: ", folder_name)
new_folder_path = Path(config.CURRENT_PATH_RGB) / self.path.name
# print("new_folder_path: ", new_folder_path)
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
# ?? Hier gewinnen wir wieder alle Verzeichnisse oberhalb der Paketebene, d.h.
# ?? unabhängig vom Päckchen
# Annahme: Wir wollen tatsächlich nur auf Päckchenebene arbeiten
# Sollten i.d.R. vier Ordner sein, 2 Kameras je 1x Vorder-/Rückseite
checkimg_folders = tuple(
p for p in self.original_data_path.rglob("checkimg") if p.is_dir()
)
# print(f">>> {checkimg_folders=}")
# images = tuple(self.original_data_path.rglob("checkimg/R_NG*_*.jpg"))
# print(f">>> {len(images)=}")
# pprint(images)
# sys.exit(0)
# iterate through all 'checkimg' folders recursively
for checkimg_folder in checkimg_folders:
# ?? Das scheint unnötig, da wir nun nur für das Päckchen relevante checkimg
# ?? Ordner durchsuchen bei jeder Iteration. Damit können wir direkt auf den
# ?? Pfaden arbeiten.
# identify the path starting from self.folder_path until the checkimg folder
# relative_path = self.extract_folder_path_within_one_AOI_folder(
# checkimg_folder, checkimg_folders, "checkimg"
# )
# save_path_rgb = new_folder_path / relative_path
# save_path_rgb.mkdir(parents=True, exist_ok=True)
relative_path = checkimg_folder.parts[-2:]
save_path_rgb = new_folder_path.joinpath(*relative_path)
save_path_rgb.mkdir(parents=True, exist_ok=True)
# print(f">>>> {relative_path=}")
# print(f">>>> {save_path_rgb=}")
for file_path in checkimg_folder.glob("R_NG*_*.jpg"):
# find match according to pattern defined at the very beginning
match = RE_CHANNEL_MAPPING.match(file_path.name)
if not match:
continue
num1, num2 = match.groups()
# find all three images belonging together
r_path = file_path
g_path = checkimg_folder / f"G_NG{num1}_{num2}.jpg"
b_path = checkimg_folder / 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]) # type: ignore
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
def create_rgb_images_and_patches_new2(self, pool: delegator.MPPool):
# 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)
# create folder name in our temp folder "Backup" and store it
# therefore, first extract the name of the current folder from the whole path
rgb_saving_path = cast(Path, Path(config.CURRENT_PATH_RGB) / self.path.name)
try:
rgb_saving_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}"
images = cast(
tuple[Path, ...], tuple(self.original_data_path.rglob("checkimg/R_NG*_*.jpg"))
)
images = pool.enrich_data_funcargs(images, rgb_saving_path)
chunks = pool.chunk_data(images)
# these are all images which must be processed
pool.map(transform_to_rgb, chunks)
return "folder_name", None
def transform_to_rgb(
files: Iterable[tuple[Path, Path]],
) -> None:
# iterable contains path to image file and the base saving path
# for RGB images
# saving_path is "new_folder_path" from above
# must be included in function call
for image, saving_path in files:
relative_path = image.parts[-3:-1]
save_path_rgb = saving_path.joinpath(*relative_path)
save_path_rgb.mkdir(parents=True, exist_ok=True)
base_folder = image.parent
assert base_folder.is_dir(), "base folder of image not a directory"
match = re.match(r"R_NG(\d+)_(\d+)\.jpg$", image.name)
if not match:
continue
num1, num2 = match.groups()
# find all three images belonging together
r_path = image
g_path = base_folder / f"G_NG{num1}_{num2}.jpg"
b_path = base_folder / 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]) # type: ignore
rgb_image = rgb_image.copy(interpretation="srgb")
filename = f"RGB_NG{num1}_{num2}.png"
rgb_image.write_to_file(save_path_rgb / filename)

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()