From 31bc9928347b72bf4da76e4520bd8e419faec02c Mon Sep 17 00:00:00 2001 From: foefl Date: Thu, 26 Feb 2026 17:58:39 +0100 Subject: [PATCH 1/8] first improvements in speedup --- README.md | 42 +-- pdm.lock | 318 ++++++++++++++++++- pyproject.toml | 4 +- src/KSG_anomaly_detection/_prepare_env.py | 32 ++ src/KSG_anomaly_detection/_profile.py | 27 ++ src/KSG_anomaly_detection/_run.py | 9 + src/KSG_anomaly_detection/config.py | 17 + src/KSG_anomaly_detection/config_for_test.py | 9 + src/KSG_anomaly_detection/gui_ai_on_off.py | 87 +++++ src/KSG_anomaly_detection/main.py | 22 ++ src/KSG_anomaly_detection/monitor.py | 179 +++++++++++ src/KSG_anomaly_detection/preparation.py | 283 +++++++++++++++++ src/KSG_anomaly_detection/window_manager.py | 22 ++ 13 files changed, 1026 insertions(+), 25 deletions(-) create mode 100644 src/KSG_anomaly_detection/_prepare_env.py create mode 100644 src/KSG_anomaly_detection/_profile.py create mode 100644 src/KSG_anomaly_detection/_run.py create mode 100644 src/KSG_anomaly_detection/config.py create mode 100644 src/KSG_anomaly_detection/config_for_test.py create mode 100644 src/KSG_anomaly_detection/gui_ai_on_off.py create mode 100644 src/KSG_anomaly_detection/main.py create mode 100644 src/KSG_anomaly_detection/monitor.py create mode 100644 src/KSG_anomaly_detection/preparation.py create mode 100644 src/KSG_anomaly_detection/window_manager.py diff --git a/README.md b/README.md index 52d4191..5a7e083 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/pdm.lock b/pdm.lock index d753077..d08a4a9 100644 --- a/pdm.lock +++ b/pdm.lock @@ -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" diff --git a/pyproject.toml b/pyproject.toml index 824f615..df8c814 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"} diff --git a/src/KSG_anomaly_detection/_prepare_env.py b/src/KSG_anomaly_detection/_prepare_env.py new file mode 100644 index 0000000..d6103b9 --- /dev/null +++ b/src/KSG_anomaly_detection/_prepare_env.py @@ -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() diff --git a/src/KSG_anomaly_detection/_profile.py b/src/KSG_anomaly_detection/_profile.py new file mode 100644 index 0000000..e68ee4c --- /dev/null +++ b/src/KSG_anomaly_detection/_profile.py @@ -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() diff --git a/src/KSG_anomaly_detection/_run.py b/src/KSG_anomaly_detection/_run.py new file mode 100644 index 0000000..e1b7209 --- /dev/null +++ b/src/KSG_anomaly_detection/_run.py @@ -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") diff --git a/src/KSG_anomaly_detection/config.py b/src/KSG_anomaly_detection/config.py new file mode 100644 index 0000000..30bd7bb --- /dev/null +++ b/src/KSG_anomaly_detection/config.py @@ -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" diff --git a/src/KSG_anomaly_detection/config_for_test.py b/src/KSG_anomaly_detection/config_for_test.py new file mode 100644 index 0000000..5b591d3 --- /dev/null +++ b/src/KSG_anomaly_detection/config_for_test.py @@ -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" +] diff --git a/src/KSG_anomaly_detection/gui_ai_on_off.py b/src/KSG_anomaly_detection/gui_ai_on_off.py new file mode 100644 index 0000000..e3f8ac9 --- /dev/null +++ b/src/KSG_anomaly_detection/gui_ai_on_off.py @@ -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() diff --git a/src/KSG_anomaly_detection/main.py b/src/KSG_anomaly_detection/main.py new file mode 100644 index 0000000..2834ef6 --- /dev/null +++ b/src/KSG_anomaly_detection/main.py @@ -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()) diff --git a/src/KSG_anomaly_detection/monitor.py b/src/KSG_anomaly_detection/monitor.py new file mode 100644 index 0000000..8646d5a --- /dev/null +++ b/src/KSG_anomaly_detection/monitor.py @@ -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) diff --git a/src/KSG_anomaly_detection/preparation.py b/src/KSG_anomaly_detection/preparation.py new file mode 100644 index 0000000..ce0cd75 --- /dev/null +++ b/src/KSG_anomaly_detection/preparation.py @@ -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 diff --git a/src/KSG_anomaly_detection/window_manager.py b/src/KSG_anomaly_detection/window_manager.py new file mode 100644 index 0000000..047296f --- /dev/null +++ b/src/KSG_anomaly_detection/window_manager.py @@ -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() From 63cb37c2544d2b00686d831490a44f51a87c5de3 Mon Sep 17 00:00:00 2001 From: foefl Date: Fri, 27 Feb 2026 08:26:48 +0100 Subject: [PATCH 2/8] remove opencv from mandatory deps --- pdm.lock | 8 ++++---- pyproject.toml | 6 +++++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/pdm.lock b/pdm.lock index d08a4a9..03b8e85 100644 --- a/pdm.lock +++ b/pdm.lock @@ -2,10 +2,10 @@ # 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:326ca1095302e816f56644c4ba0929ea12b930348375c98eba139701e0388de6" +content_hash = "sha256:dd33ca3d0a561a8f2634539cc333c0456a89a60141b902cb78408bf546abbc85" [[metadata.targets]] requires_python = ">=3.11,<3.15" @@ -1547,7 +1547,7 @@ name = "numpy" version = "2.4.2" requires_python = ">=3.11" summary = "Fundamental package for array computing in Python" -groups = ["default"] +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"}, @@ -1628,7 +1628,7 @@ name = "opencv-python" version = "4.13.0.92" requires_python = ">=3.6" summary = "Wrapper package for OpenCV python bindings." -groups = ["default"] +groups = ["open-cv"] dependencies = [ "numpy<2.0; python_version < \"3.9\"", "numpy>=2; python_version >= \"3.9\"", diff --git a/pyproject.toml b/pyproject.toml index df8c814..c8794d5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 = ["PySide6>=6.10.2", "numpy>=2.4.2", "pillow>=12.1.1", "opencv-python>=4.13.0.92", "pyvips[binary]>=3.1.1"] +dependencies = ["PySide6>=6.10.2", "numpy>=2.4.2", "pillow>=12.1.1", "pyvips[binary]>=3.1.1"] 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" From cc664b036960d7d9e4124bf14f425d79df615ce9 Mon Sep 17 00:00:00 2001 From: foefl Date: Fri, 27 Feb 2026 10:30:29 +0100 Subject: [PATCH 3/8] add notes and simplify routines --- src/KSG_anomaly_detection/_profile.py | 6 +- src/KSG_anomaly_detection/monitor.py | 9 ++- src/KSG_anomaly_detection/preparation.py | 87 ++++++++++++++++++++---- 3 files changed, 88 insertions(+), 14 deletions(-) diff --git a/src/KSG_anomaly_detection/_profile.py b/src/KSG_anomaly_detection/_profile.py index e68ee4c..9a80999 100644 --- a/src/KSG_anomaly_detection/_profile.py +++ b/src/KSG_anomaly_detection/_profile.py @@ -8,17 +8,21 @@ profiler = cProfile.Profile() PROFILE = True USE_NEW_IMPL = True +ONLY_PREPARE = False def main() -> None: _prepare_env.main() + if ONLY_PREPARE: + return + 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) + stats.print_stats(20) else: monitor_folder_simple(use_new=USE_NEW_IMPL) diff --git a/src/KSG_anomaly_detection/monitor.py b/src/KSG_anomaly_detection/monitor.py index 8646d5a..c21d540 100644 --- a/src/KSG_anomaly_detection/monitor.py +++ b/src/KSG_anomaly_detection/monitor.py @@ -1,5 +1,6 @@ import os import re +import sys import time import traceback from pathlib import Path @@ -146,7 +147,11 @@ def monitor_folder_simple(use_new: bool): # 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 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 @@ -157,6 +162,8 @@ def monitor_folder_simple(use_new: bool): 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 print("'create_rgb_images_and_patches'...") diff --git a/src/KSG_anomaly_detection/preparation.py b/src/KSG_anomaly_detection/preparation.py index ce0cd75..7917199 100644 --- a/src/KSG_anomaly_detection/preparation.py +++ b/src/KSG_anomaly_detection/preparation.py @@ -18,16 +18,20 @@ 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/... + # 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/... + self.original_data_path = Path(config.STORING_PATH).joinpath(*self.path.parts[-4:-1]) print(f"[{self.visper}] {Path(self.folder_path).name} Vorbereitung gestartet...") @@ -46,7 +50,6 @@ class Preparation: 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 @@ -70,6 +73,59 @@ class Preparation: 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_path / self.path.name + copytree(src, dst, 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 + # return Path(config.STORING_PATH) / self.visper / Path( + # self.folder_path + # ).parent.parent / Path(self.folder_path).parent, None + return self.original_data_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 @@ -108,7 +164,14 @@ class Preparation: return img def change_image_to_yellow_new(self): - base_path = Path(self.folder_path).parent + # ?? 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 From 98d340ff0139fa986bd58f22784ee2f54a56e67a Mon Sep 17 00:00:00 2001 From: foefl Date: Fri, 27 Feb 2026 15:07:30 +0100 Subject: [PATCH 4/8] rework unneeded checks and iterations --- src/KSG_anomaly_detection/preparation.py | 70 +++++++++++++++++------- 1 file changed, 51 insertions(+), 19 deletions(-) diff --git a/src/KSG_anomaly_detection/preparation.py b/src/KSG_anomaly_detection/preparation.py index 7917199..58947ce 100644 --- a/src/KSG_anomaly_detection/preparation.py +++ b/src/KSG_anomaly_detection/preparation.py @@ -1,5 +1,6 @@ import os import re +import sys import traceback from pathlib import Path from pprint import pprint @@ -31,7 +32,12 @@ class Preparation: # / Path(self.folder_path).parents[1].name # / Path(self.folder_path).parent.name # ) # Pfad zu Fileserver/KI/... - self.original_data_path = Path(config.STORING_PATH).joinpath(*self.path.parts[-4:-1]) + + # 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...") @@ -105,16 +111,17 @@ class Preparation: # copytree(src, dst, dirs_exist_ok=True) src = self.path - dst = self.original_data_path / self.path.name - copytree(src, dst, dirs_exist_ok=True) + # 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_path, None + return self.original_data_base_path, None except FileExistsError: return ( @@ -214,10 +221,19 @@ class Preparation: 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_path) + relative_path = current_folder_to_inspect.relative_to( + self.original_data_base_path + ) relative_path = Path(*relative_path.parts[1:]) return relative_path @@ -243,7 +259,7 @@ class Preparation: # find all checkimg folders within the folder checkimg_folders = [ - p for p in self.original_data_path.rglob("checkimg") if p.is_dir() + p for p in self.original_data_base_path.rglob("checkimg") if p.is_dir() ] # iterate through all 'checkimg' folders recursively @@ -293,8 +309,10 @@ class Preparation: # 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 + # 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) @@ -307,21 +325,35 @@ class Preparation: return None, f"Fehlermeldung: {e}" # find all checkimg folders within the folder - checkimg_folders = [ + # ?? 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 + checkimg_folders = tuple( p for p in self.original_data_path.rglob("checkimg") if p.is_dir() - ] + ) + # print(f">>> {checkimg_folders=}") + + # sys.exit(0) # iterate through all 'checkimg' folders recursively - for current_folder_to_inspect in checkimg_folders: + 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( - current_folder_to_inspect, checkimg_folders, "checkimg" - ) + # 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) - save_path_rgb = new_folder_path / relative_path + 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 current_folder_to_inspect.glob("R_NG*_*.jpg"): + for file_path in checkimg_folder.glob("R_NG*_*.jpg"): # find match according to pattern defined at the very beginning match = pattern.match(file_path.name) if not match: @@ -331,14 +363,14 @@ class Preparation: # 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" + 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]) + 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) From c3249ed60e475101a69cf9ecac55dad5789f3c19 Mon Sep 17 00:00:00 2001 From: foefl Date: Fri, 27 Feb 2026 15:07:40 +0100 Subject: [PATCH 5/8] add additional package folder --- src/KSG_anomaly_detection/_prepare_env.py | 28 +++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/KSG_anomaly_detection/_prepare_env.py b/src/KSG_anomaly_detection/_prepare_env.py index d6103b9..8aaf35f 100644 --- a/src/KSG_anomaly_detection/_prepare_env.py +++ b/src/KSG_anomaly_detection/_prepare_env.py @@ -16,16 +16,36 @@ def recreate_folder(folder_name: str) -> Path: def main() -> None: + paths_src: list[Path] = [] + paths_dst: list[Path] = [] + _ = recreate_folder("Daten") _ = recreate_folder("KI") - p_data = recreate_folder( - "./Verifizierdaten_1/20260225/614706_helles Entek/614706_helles Entek[3136761]_1" - ) + # packages 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) + 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) + + for src, dst in zip(paths_src, paths_dst): + shutil.copytree(src, dst, dirs_exist_ok=True) if __name__ == "__main__": From b505bd7076afaf0cbe245fbd18fd0452d1970ef6 Mon Sep 17 00:00:00 2001 From: foefl Date: Fri, 27 Feb 2026 15:08:02 +0100 Subject: [PATCH 6/8] more specific print statement --- src/KSG_anomaly_detection/monitor.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/KSG_anomaly_detection/monitor.py b/src/KSG_anomaly_detection/monitor.py index c21d540..2f78f34 100644 --- a/src/KSG_anomaly_detection/monitor.py +++ b/src/KSG_anomaly_detection/monitor.py @@ -127,7 +127,7 @@ def monitor_folder(manager: WindowManager): def monitor_folder_simple(use_new: bool): - print("starting thread...") + print("starting procedure...") for folder in config_for_test.FOLDER_LIST: # try: @@ -157,20 +157,26 @@ def monitor_folder_simple(use_new: bool): # 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() + SKIP_NEXT = True + if not SKIP_NEXT: + 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 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 not SKIP_NEXT: + 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") From a9c6e4a260445296f38198717d6317ad7a33bfa2 Mon Sep 17 00:00:00 2001 From: foefl Date: Fri, 27 Feb 2026 16:19:52 +0100 Subject: [PATCH 7/8] prepare multiprocessing --- pdm.lock | 4 +-- pyproject.toml | 2 +- src/KSG_anomaly_detection/_profile.py | 2 +- src/KSG_anomaly_detection/config_for_test.py | 3 +- src/KSG_anomaly_detection/delegator.py | 36 ++++++++++++++++++++ src/KSG_anomaly_detection/preparation.py | 11 +++--- 6 files changed, 49 insertions(+), 9 deletions(-) create mode 100644 src/KSG_anomaly_detection/delegator.py diff --git a/pdm.lock b/pdm.lock index 03b8e85..48a8942 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "dev", "lint", "nb", "open-cv", "tests"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:dd33ca3d0a561a8f2634539cc333c0456a89a60141b902cb78408bf546abbc85" +content_hash = "sha256:3c47abf04bea7dfd195f350d89b59416b8492aaaf54257badfb8b4814b20e996" [[metadata.targets]] requires_python = ">=3.11,<3.15" @@ -1862,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"}, diff --git a/pyproject.toml b/pyproject.toml index c8794d5..170fbd1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ authors = [ {name = "Susanne Franke", email = "s.franke@d-opt.de"}, {name = "Florian Förster", email = "f.foerster@d-opt.com"}, ] -dependencies = ["PySide6>=6.10.2", "numpy>=2.4.2", "pillow>=12.1.1", "pyvips[binary]>=3.1.1"] +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"} diff --git a/src/KSG_anomaly_detection/_profile.py b/src/KSG_anomaly_detection/_profile.py index 9a80999..6253f55 100644 --- a/src/KSG_anomaly_detection/_profile.py +++ b/src/KSG_anomaly_detection/_profile.py @@ -6,7 +6,7 @@ from KSG_anomaly_detection.monitor import monitor_folder_simple profiler = cProfile.Profile() -PROFILE = True +PROFILE = False USE_NEW_IMPL = True ONLY_PREPARE = False diff --git a/src/KSG_anomaly_detection/config_for_test.py b/src/KSG_anomaly_detection/config_for_test.py index 5b591d3..cacc114 100644 --- a/src/KSG_anomaly_detection/config_for_test.py +++ b/src/KSG_anomaly_detection/config_for_test.py @@ -5,5 +5,6 @@ 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]_1", + r"B:\projects\KSG\Ordnerstruktur\Verifizierdaten_1\20260225\614706_helles Entek\614706_helles Entek[3136761]_2", ] diff --git a/src/KSG_anomaly_detection/delegator.py b/src/KSG_anomaly_detection/delegator.py new file mode 100644 index 0000000..4f913f1 --- /dev/null +++ b/src/KSG_anomaly_detection/delegator.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +import multiprocessing as mp +from collections.abc import Iterable, Sequence +from typing import Any, TypeVar + +import psutil + +T = TypeVar("T") + + +class MPPool: + def __init__(self) -> None: + self.num_workers = psutil.cpu_count(logical=False) or 4 + self.pool = mp.Pool(processes=self.num_workers) + + def chunk_data( + self, + data: list[T], + chunk_size: int | None = None, + ) -> Sequence[Sequence[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)] + + if len(chunks) > self.num_workers: + open_chunk = chunks[-1] + for idx, entry in enumerate(open_chunk): + chunks[idx].append(entry) + del chunks[-1] + + return chunks + + def stop(self) -> None: + self.pool.close() + self.pool.join() diff --git a/src/KSG_anomaly_detection/preparation.py b/src/KSG_anomaly_detection/preparation.py index 58947ce..4f2b35a 100644 --- a/src/KSG_anomaly_detection/preparation.py +++ b/src/KSG_anomaly_detection/preparation.py @@ -15,6 +15,7 @@ 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]} +RE_CHANNEL_MAPPING = re.compile(r"R_NG(\d+)_(\d+)\.jpg$") class Preparation: @@ -305,7 +306,6 @@ class Preparation: 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 @@ -328,12 +328,16 @@ class Preparation: # ?? 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=}") - # sys.exit(0) + images = tuple(self.original_data_path.rglob("checkimg/R_NG*_*.jpg")) + print(f">>> {len(images)=}") + + sys.exit(0) # iterate through all 'checkimg' folders recursively for checkimg_folder in checkimg_folders: @@ -355,10 +359,9 @@ class Preparation: for file_path in checkimg_folder.glob("R_NG*_*.jpg"): # find match according to pattern defined at the very beginning - match = pattern.match(file_path.name) + match = RE_CHANNEL_MAPPING.match(file_path.name) if not match: continue - num1, num2 = match.groups() # find all three images belonging together From 13c3c432615f27eb9c52ce3b8729704c082200e1 Mon Sep 17 00:00:00 2001 From: foefl Date: Mon, 2 Mar 2026 15:47:44 +0100 Subject: [PATCH 8/8] working prototype with significant speed-up --- src/KSG_anomaly_detection/_prepare_env.py | 11 +++ src/KSG_anomaly_detection/_profile.py | 34 ++++++--- src/KSG_anomaly_detection/config_for_test.py | 1 + src/KSG_anomaly_detection/delegator.py | 61 +++++++++++++--- src/KSG_anomaly_detection/monitor.py | 22 ++++-- src/KSG_anomaly_detection/preparation.py | 77 ++++++++++++++++++-- 6 files changed, 173 insertions(+), 33 deletions(-) diff --git a/src/KSG_anomaly_detection/_prepare_env.py b/src/KSG_anomaly_detection/_prepare_env.py index 8aaf35f..7bfbaaf 100644 --- a/src/KSG_anomaly_detection/_prepare_env.py +++ b/src/KSG_anomaly_detection/_prepare_env.py @@ -44,6 +44,17 @@ def main() -> None: ) 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) diff --git a/src/KSG_anomaly_detection/_profile.py b/src/KSG_anomaly_detection/_profile.py index 6253f55..246ede4 100644 --- a/src/KSG_anomaly_detection/_profile.py +++ b/src/KSG_anomaly_detection/_profile.py @@ -1,13 +1,15 @@ import cProfile import pstats +import time -from KSG_anomaly_detection import _prepare_env +from KSG_anomaly_detection import _prepare_env, delegator from KSG_anomaly_detection.monitor import monitor_folder_simple profiler = cProfile.Profile() -PROFILE = False -USE_NEW_IMPL = True +PROFILE = True +USE_NEW_IMPL = False +USE_MP = False ONLY_PREPARE = False @@ -16,15 +18,25 @@ def main() -> None: if ONLY_PREPARE: return - if PROFILE: - profiler.enable() - monitor_folder_simple(use_new=USE_NEW_IMPL) - profiler.disable() + mp_pool = delegator.MPPool() - stats = pstats.Stats(profiler).sort_stats("cumtime") - stats.print_stats(20) - else: - monitor_folder_simple(use_new=USE_NEW_IMPL) + 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__": diff --git a/src/KSG_anomaly_detection/config_for_test.py b/src/KSG_anomaly_detection/config_for_test.py index cacc114..036e8a6 100644 --- a/src/KSG_anomaly_detection/config_for_test.py +++ b/src/KSG_anomaly_detection/config_for_test.py @@ -7,4 +7,5 @@ PATH = r"B:\projects\KSG\Ordnerstruktur" 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", ] diff --git a/src/KSG_anomaly_detection/delegator.py b/src/KSG_anomaly_detection/delegator.py index 4f913f1..12e33c3 100644 --- a/src/KSG_anomaly_detection/delegator.py +++ b/src/KSG_anomaly_detection/delegator.py @@ -1,36 +1,77 @@ from __future__ import annotations +import math import multiprocessing as mp -from collections.abc import Iterable, Sequence +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: + 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, - ) -> Sequence[Sequence[T]]: + ) -> 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: - open_chunk = chunks[-1] - for idx, entry in enumerate(open_chunk): - chunks[idx].append(entry) - del chunks[-1] + 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) - return chunks + for idx, entry in enumerate(open_entries): + chunks_assigned[idx].append(entry) - def stop(self) -> None: + 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() diff --git a/src/KSG_anomaly_detection/monitor.py b/src/KSG_anomaly_detection/monitor.py index 2f78f34..be0777f 100644 --- a/src/KSG_anomaly_detection/monitor.py +++ b/src/KSG_anomaly_detection/monitor.py @@ -5,7 +5,7 @@ import time import traceback from pathlib import Path -from KSG_anomaly_detection import config, config_for_test +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 @@ -126,7 +126,7 @@ def monitor_folder(manager: WindowManager): time.sleep(60) -def monitor_folder_simple(use_new: bool): +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: @@ -156,9 +156,9 @@ def monitor_folder_simple(use_new: bool): 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'...") - SKIP_NEXT = True + SKIP_NEXT = False if not SKIP_NEXT: + print("'change_image_to_yellow'...") if use_new: preparation.change_image_to_yellow_new() else: @@ -168,9 +168,9 @@ def monitor_folder_simple(use_new: bool): # 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'...") SKIP_NEXT = False - if not SKIP_NEXT: + 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() @@ -178,6 +178,16 @@ def monitor_folder_simple(use_new: bool): 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: diff --git a/src/KSG_anomaly_detection/preparation.py b/src/KSG_anomaly_detection/preparation.py index 4f2b35a..96e508f 100644 --- a/src/KSG_anomaly_detection/preparation.py +++ b/src/KSG_anomaly_detection/preparation.py @@ -1,17 +1,19 @@ +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, cast +from typing import Literal, TypeAlias, cast import pyvips from PIL import Image from pyvips import Image as vipsImage -from KSG_anomaly_detection import config +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]} @@ -334,10 +336,10 @@ class Preparation: ) # print(f">>> {checkimg_folders=}") - images = tuple(self.original_data_path.rglob("checkimg/R_NG*_*.jpg")) - print(f">>> {len(images)=}") - - sys.exit(0) + # 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: @@ -379,3 +381,66 @@ class Preparation: 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)