Repository: HakaiInstitute/GlintMaskGenerator (language: python)

Issue:
Update to work with Python 3.11
**Is your feature request related to a problem? Please describe.**
GlintMaskGenerator does not work with Python 3.11

**Describe the solution you'd like**
Update the project to be able to support Python 3.11

**Describe alternatives you've considered**
I have been able to run GlintMaskGenerator successfully with Python 3.11, just by changing 2 np.float and 1 np.int references to float and int respectively. 

Hello! 

Just wanted to post this here. At the current state, GlintMaskTools does not support Python 3.11

However, as mentioned above, I was able to successfully run GlintMaskTools with Python 3.11 after some fairly minor changes. 

Could this change be implemented? I can also give more information into what I changed within the current code if needed.



Discussion / hints:
Hey @Latseen , absolutely this can and should be done. Did you want to open a pull request with your changes and I can try to get that done this week?

Fix patch:
diff --git a/.github/workflows/release-packaging.yml b/.github/workflows/release-packaging.yml
index 38a8d2a..9e0c85e 100644
--- a/.github/workflows/release-packaging.yml
+++ b/.github/workflows/release-packaging.yml
@@ -39,13 +39,13 @@ jobs:
       - name: Install Linux dependencies
         if: ${{ runner.os == 'Linux' }}
         run: >
-          sudo apt-get install 
-          '^libxcb.*-dev' 
-          libx11-xcb-dev 
-          libglu1-mesa-dev 
-          libxrender-dev 
-          libxi-dev 
-          libxkbcommon-dev 
+          sudo apt-get install
+          '^libxcb.*-dev'
+          libx11-xcb-dev
+          libglu1-mesa-dev
+          libxrender-dev
+          libxi-dev
+          libxkbcommon-dev
           libxkbcommon-x11-dev
 
       - name: Install dependencies
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..9e4fb8a
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,23 @@
+# See https://pre-commit.com for more information
+# See https://pre-commit.com/hooks.html for more hooks
+repos:
+  - repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v4.4.0
+    hooks:
+      - id: trailing-whitespace
+      - id: end-of-file-fixer
+      - id: check-added-large-files
+  - repo: https://github.com/python-poetry/poetry
+    rev:  1.4.0
+    hooks:
+      - id: poetry-check
+      - id: poetry-lock
+  - repo: https://github.com/charliermarsh/ruff-pre-commit
+    rev: v0.0.265
+    hooks:
+      - id: ruff
+        args: [ --fix ]
+  - repo: https://github.com/psf/black
+    rev: 23.3.0
+    hooks:
+      - id: black
diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md
index dbb43fc..e95833e 100644
--- a/DEVELOPMENT.md
+++ b/DEVELOPMENT.md
@@ -7,7 +7,7 @@ To install `glint-mask-tools` PyPi package on Windows, users will have to first
 ### Installation
 1. Clone the code repository from [GitHub](https://github.com/HakaiInstitute/GlintMaskGenerator).
 2. You must have the Poetry package manager installed. Do that. [Instructions here](https://python-poetry.org/)
-3. Create a Poetry environment and install the packages listed in `pyproject.toml` file of the GlintMaskGenerator repo. 
+3. Create a Poetry environment and install the packages listed in `pyproject.toml` file of the GlintMaskGenerator repo.
    1.Run `poetry install`.
 
 ### Running the code
@@ -15,17 +15,17 @@ To install `glint-mask-tools` PyPi package on Windows, users will have to first
    1. e.g. Run `poetry run python src/gui.py` to execute the GUI script.
 
 ### Project structure
-- The bulk of the actual processing logic takes place in the files found in the "src/glint_mask_tools" directory. 
-    - The functions in this directory are carefully documented with comments and function help strings. 
-    - The strings at the beginning of each function is ingested by the "python-fire" package to create help strings for 
+- The bulk of the actual processing logic takes place in the files found in the "src/glint_mask_tools" directory.
+    - The functions in this directory are carefully documented with comments and function help strings.
+    - The strings at the beginning of each function is ingested by the "python-fire" package to create help strings for
     the command line interface. Updating these strings in `src/glint_mask_tools/cli.py` will update the CLI help output.
-- The "tests/" directory contains some files for testing those functions found in "src/glint_mask_tools". 
-    - Tests can be run with pytest. Run `poetry run pytest` from the terminal to run the test functions. 
+- The "tests/" directory contains some files for testing those functions found in "src/glint_mask_tools".
+    - Tests can be run with pytest. Run `poetry run pytest` from the terminal to run the test functions.
     - All functions beginning with the word "test_" are taken as test functions by pytest.
 - The code is written in using object-oriented principles. So, e.g. to support a new kind of Red Edge-based glint masking,
     what is needed is to write a new child class of the `ImageLoader` class located in `src/glint_mask_tools/image_loaders.py`,
     and then use this new loader class in the declaration of a new `Masker` class in `src/glint_mask_tools/maskers.py`.
-    - After implementing a new Masker child class, add it to the GUI using QTDesigner to edit the `src/resources/gui.ui` file. 
+    - After implementing a new Masker child class, add it to the GUI using QTDesigner to edit the `src/resources/gui.ui` file.
     - The logic for how the GUI executes the library code is located in `src/gui.py`, and will also need to be updated to use the new `Masker`.
 
 ### Updating the executable files
@@ -34,12 +34,12 @@ This is done automatically via GitHub Actions scripts. The workflow to trigger t
 1. Make the code changes and commit them to GitHub.
     1. Ideally, use a Pull-Request workflow. Make a pull-request to the master branch from some other branch on GitHub.
     2. This will run the tests.
-2. Once the tests are passing, tag the main branch on your copy locally with `git checkout main && git pull && git tag v*.*.*` 
+2. Once the tests are passing, tag the main branch on your copy locally with `git checkout main && git pull && git tag v*.*.*`
     using an appropriate semantic version number. The `pyproject.toml` is updated with this version by the actions script at build time.
-3. Push the tag to GitHub with `git push --tags`. Pushing a tag of the format `v*.*.*` triggers an action where the code is 
+3. Push the tag to GitHub with `git push --tags`. Pushing a tag of the format `v*.*.*` triggers an action where the code is
     checked to see tests are passing, then executables for Linux and Windows are built and uploaded to the GitHub release page.
 4. Similarly, the PyPi package will be built and published using Poetry commands defined by the `pypi-publish.yml`
     GitHub Actions definition file.
 
 ---
-*Created by Taylor Denouden @ Hakai Institute on 2022-11-30*
\ No newline at end of file
+*Created by Taylor Denouden @ Hakai Institute on 2022-11-30*
diff --git a/README.md b/README.md
index 1dc931f..92dccbc 100644
--- a/README.md
+++ b/README.md
@@ -63,30 +63,30 @@ glint-mask-v*.*.* --help
 
 # NAME
 #     glint-mask-v*.*.*
-# 
+#
 # SYNOPSIS
 #     glint-mask-v*.*.* - COMMAND | VALUE
-# 
+#
 # COMMANDS
 #     COMMAND is one of the following:
-# 
+#
 #      cir_threshold
 #        Generate masks for glint regions in 4 Band CIR imagery using Tom Bell's binning algorithm.
-# 
+#
 #      micasense_threshold
 #        Generate masks for glint regions in multispectral imagery from the Micasense camera using Tom Bell's algorithm on the blue image band.
-# 
+#
 #      p4ms_threshold
 #        Generate masks for glint regions in multispectral imagery from the DJI camera using Tom Bell's algorithm on the Blue image band.
-# 
+#
 #      process
-# 
+#
 #      rgb_threshold
 #        Generate masks for glint regions in RGB imagery using Tom Bell's binning algorithm.
-# 
+#
 # VALUES
 #     VALUE is one of the following:
-# 
+#
 #      max_workers
 #        The maximum number of threads to use for processing.
 ```
@@ -97,25 +97,25 @@ glint-mask-v*.*.* rgb_threshold --help
 
 # NAME
 #     glint-mask-v*.*.* rgb_threshold - Generate masks for glint regions in RGB imagery using Tom Bell's binning algorithm.
-# 
+#
 # SYNOPSIS
 #     glint-mask-v*.*.* rgb_threshold IMG_DIR OUT_DIR <flags>
-# 
+#
 # DESCRIPTION
 #     Generate masks for glint regions in RGB imagery using Tom Bell's binning algorithm.
-# 
+#
 # POSITIONAL ARGUMENTS
 #     IMG_DIR
 #         The path to a named input image or directory containing images. If img_dir is a directory, all tif, jpg, jpeg, and png images in that directory will be # processed.
 #     OUT_DIR
 #         The path to send your out image including the file name and type. e.g. "/path/to/mask.png". out_dir must be a directory if img_dir is specified as a # # # directory.
-# 
+#
 # FLAGS
 #     --thresholds=THRESHOLDS
 #         The pixel band thresholds indicating glint. Domain for values is (0.0, 1.0). Default is [1, 1, 0.875].
 #     --pixel_buffer=PIXEL_BUFFER
 #         The pixel distance to buffer out the mask. Defaults to 0 (off).
-# 
+#
 # NOTES
 #     You can also use flags syntax for POSITIONAL ARGUMENTS
 ```
@@ -130,7 +130,7 @@ glint-mask-v*.*.* aco_threshold /path/to/dir/with/images/ /path/to/out_masks/dir
 # Process DJI P4MS imagery
 glint-mask-v*.*.* p4ms_threshold /path/to/dir/with/images/ /path/to/out_masks/dir/
 
-# Process Micasense RedEdge imagery 
+# Process Micasense RedEdge imagery
 glint-mask-v*.*.* micasense_threshold /path/to/dir/with/images/ /path/to/out_masks/dir/
 ```
 
diff --git a/glint_mask_generator/__init__.py b/glint_mask_generator/__init__.py
index 17b0880..3afe314 100644
--- a/glint_mask_generator/__init__.py
+++ b/glint_mask_generator/__init__.py
@@ -4,9 +4,19 @@
 Date: 2020-09-18
 """
 
-from .maskers import Masker, RGBThresholdMasker, P4MSThresholdMasker, \
-    MicasenseRedEdgeThresholdMasker, CIRThresholdMasker
+from .maskers import (
+    Masker,
+    RGBThresholdMasker,
+    P4MSThresholdMasker,
+    MicasenseRedEdgeThresholdMasker,
+    CIRThresholdMasker,
+)
 
-__all__ = ["Masker", "RGBThresholdMasker", "P4MSThresholdMasker", "MicasenseRedEdgeThresholdMasker",
-           "CIRThresholdMasker"]
+__all__ = [
+    "Masker",
+    "RGBThresholdMasker",
+    "P4MSThresholdMasker",
+    "MicasenseRedEdgeThresholdMasker",
+    "CIRThresholdMasker",
+]
 __version__ = "0.0.0"
diff --git a/glint_mask_generator/cli.py b/glint_mask_generator/cli.py
index 1f7d942..b5f55ef 100644
--- a/glint_mask_generator/cli.py
+++ b/glint_mask_generator/cli.py
@@ -11,7 +11,13 @@
 import fire
 from tqdm import tqdm
 
-from .maskers import Masker, MicasenseRedEdgeThresholdMasker, P4MSThresholdMasker, RGBThresholdMasker, CIRThresholdMasker
+from .maskers import (
+    Masker,
+    MicasenseRedEdgeThresholdMasker,
+    P4MSThresholdMasker,
+    RGBThresholdMasker,
+    CIRThresholdMasker,
+)
 
 
 class CLI(object):
@@ -31,24 +37,35 @@ def _err_callback(path, exception):
 
     def _process(self, masker: Masker):
         with tqdm(total=len(masker)) as progress:
-            masker(max_workers=self.max_workers,
-                   callback=lambda _: progress.update(1),
-                   err_callback=self._err_callback)
-
-    def rgb_threshold(self, img_dir: str, out_dir: str, thresholds: List[float] = (1, 1, 0.875),
-                      pixel_buffer: int = 0) -> None:
-        """Generate masks for glint regions in RGB imagery using Tom Bell's binning algorithm.
+            masker(
+                max_workers=self.max_workers,
+                callback=lambda _: progress.update(1),
+                err_callback=self._err_callback,
+            )
+
+    def rgb_threshold(
+        self,
+        img_dir: str,
+        out_dir: str,
+        thresholds: List[float] = (1, 1, 0.875),
+        pixel_buffer: int = 0,
+    ) -> None:
+        """Generate masks for glint regions in RGB imagery using Tom Bell's binning
+            algorithm.
 
         Parameters
         ----------
         img_dir
-            The path to a named input image or directory containing images. If img_dir is a directory, all tif, jpg, jpeg,
-            and png images in that directory will be processed.
+            The path to a named input image or directory containing images. If img_dir
+            is a directory, all tif, jpg, jpeg, and png images in that directory will
+            be processed.
         out_dir
-            The path to send your out image including the file name and type. e.g. "/path/to/mask.png".
-            out_dir must be a directory if img_dir is specified as a directory.
+            The path to send your out image including the file name and type.
+            e.g. "/path/to/mask.png". out_dir must be a directory if img_dir is
+            specified as a directory.
         thresholds
-            The pixel band thresholds indicating glint. Domain for values is (0.0, 1.0). Default is [1, 1, 0.875].
+            The pixel band thresholds indicating glint. Domain for values is (0.0, 1.0).
+            Default is [1, 1, 0.875].
         pixel_buffer
             The pixel distance to buffer out the mask. Defaults to 0 (off).
 
@@ -59,20 +76,29 @@ def rgb_threshold(self, img_dir: str, out_dir: str, thresholds: List[float] = (1
         """
         self._process(RGBThresholdMasker(img_dir, out_dir, thresholds, pixel_buffer))
 
-    def cir_threshold(self, img_dir: str, out_dir: str, thresholds: List[float] = (1, 1, 0.875, 1),
-                      pixel_buffer: int = 0) -> None:
-        """Generate masks for glint regions in 4 Band CIR imagery using Tom Bell's binning algorithm.
+    def cir_threshold(
+        self,
+        img_dir: str,
+        out_dir: str,
+        thresholds: List[float] = (1, 1, 0.875, 1),
+        pixel_buffer: int = 0,
+    ) -> None:
+        """Generate masks for glint regions in 4 Band CIR imagery using Tom Bell's
+            binning algorithm.
 
         Parameters
         ----------
         img_dir
-            The path to a named input image or directory containing images. If img_dir is a directory, all tif, jpg, jpeg,
-            and png images in that directory will be processed.
+            The path to a named input image or directory containing images. If img_dir
+            is a directory, all tif, jpg, jpeg, and png images in that directory will
+            be processed.
         out_dir
-            The path to send your out image including the file name and type. e.g. "/path/to/mask.png".
-            out_dir must be a directory if img_dir is specified as a directory.
+            The path to send your out image including the file name and type.
+            e.g. "/path/to/mask.png". out_dir must be a directory if img_dir is
+            specified as a directory.
         thresholds
-            The pixel band thresholds indicating glint. Domain for values is (0.0, 1.0). Default is [1, 1, 0.875].
+            The pixel band thresholds indicating glint. Domain for values is (0.0, 1.0).
+            Default is [1, 1, 0.875].
         pixel_buffer
             The pixel distance to buffer out the mask. Defaults to 0 (off).
 
@@ -83,21 +109,29 @@ def cir_threshold(self, img_dir: str, out_dir: str, thresholds: List[float] = (1
         """
         self._process(CIRThresholdMasker(img_dir, out_dir, thresholds, pixel_buffer))
 
-    def p4ms_threshold(self, img_dir: str, out_dir: str, thresholds: List[float] = (0.875, 1, 1, 1, 1),
-                       pixel_buffer: int = 0) -> None:
-        """Generate masks for glint regions in multispectral imagery from the DJI camera using Tom Bell's algorithm on
-            the Blue image band.
+    def p4ms_threshold(
+        self,
+        img_dir: str,
+        out_dir: str,
+        thresholds: List[float] = (0.875, 1, 1, 1, 1),
+        pixel_buffer: int = 0,
+    ) -> None:
+        """Generate masks for glint regions in multispectral imagery from the DJI camera
+            using Tom Bell's algorithm on the Blue image band.
 
         Parameters
         ----------
         img_dir
-            The path to a named input image or directory containing images. If img_dir is a directory, all tif, jpg, jpeg,
-            and png images in that directory will be processed.
+            The path to a named input image or directory containing images. If img_dir
+            is a directory, all tif, jpg, jpeg, and png images in that directory will
+            be processed.
         out_dir
-            The path to send your out image including the file name and type. e.g. "/path/to/mask.png".
-            out_dir must be a directory if img_dir is specified as a directory.
+            The path to send your out image including the file name and type.
+            e.g. "/path/to/mask.png". out_dir must be a directory if img_dir is
+            specified as a directory.
         thresholds
-            The pixel band thresholds indicating glint. Domain for values is (0.0, 1.0). Default is [0.875, 1, 1, 1, 1].
+            The pixel band thresholds indicating glint. Domain for values is (0.0, 1.0).
+            Default is [0.875, 1, 1, 1, 1].
         pixel_buffer
             The pixel distance to buffer out the mask. Defaults to 0 (off).
 
@@ -108,21 +142,29 @@ def p4ms_threshold(self, img_dir: str, out_dir: str, thresholds: List[float] = (
         """
         self._process(P4MSThresholdMasker(img_dir, out_dir, thresholds, pixel_buffer))
 
-    def micasense_threshold(self, img_dir: str, out_dir: str, thresholds: List[float] = (0.875, 1, 1, 1, 1),
-                            pixel_buffer: int = 0) -> None:
-        """Generate masks for glint regions in multispectral imagery from the Micasense camera using Tom Bell's
-            algorithm on the blue image band.
+    def micasense_threshold(
+        self,
+        img_dir: str,
+        out_dir: str,
+        thresholds: List[float] = (0.875, 1, 1, 1, 1),
+        pixel_buffer: int = 0,
+    ) -> None:
+        """Generate masks for glint regions in multispectral imagery from the
+            Micasense camera using Tom Bell's algorithm on the blue image band.
 
         Parameters
         ----------
         img_dir
-            The path to a named input image or directory containing images. If img_dir is a directory, all tif, jpg, jpeg,
-            and png images in that directory will be processed.
+            The path to a named input image or directory containing images.
+            If img_dir is a directory, all tif, jpg, jpeg, and png images in that
+            directory will be processed.
         out_dir
-            The path to send your out image including the file name and type. e.g. "/path/to/mask.png".
-            out_dir must be a directory if img_dir is specified as a directory.
+            The path to send your out image including the file name and type.
+            e.g. "/path/to/mask.png". out_dir must be a directory if img_dir is
+            specified as a directory.
         thresholds
-            The pixel band thresholds indicating glint. Domain for values is (0.0, 1.0). Default is [0.875, 1, 1, 1, 1].
+            The pixel band thresholds indicating glint. Domain for values is (0.0, 1.0).
+            Default is [0.875, 1, 1, 1, 1].
         pixel_buffer
             The pixel distance to buffer out the mask. Defaults to 0 (off).
 
@@ -131,12 +173,14 @@ def micasense_threshold(self, img_dir: str, out_dir: str, thresholds: List[float
         None
             Side effects are that the mask is saved to the specified out_dir location.
         """
-        self._process(MicasenseRedEdgeThresholdMasker(img_dir, out_dir, thresholds, pixel_buffer))
+        self._process(
+            MicasenseRedEdgeThresholdMasker(img_dir, out_dir, thresholds, pixel_buffer)
+        )
 
 
 def main():
     fire.Fire(CLI)
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     main()
diff --git a/glint_mask_generator/glint_algorithms.py b/glint_mask_generator/glint_algorithms.py
index 4272511..4b11406 100644
--- a/glint_mask_generator/glint_algorithms.py
+++ b/glint_mask_generator/glint_algorithms.py
@@ -18,7 +18,9 @@ def __init__(self):
 
     @abstractmethod
     def __call__(self, img: np.ndarray) -> np.ndarray:
-        """Return a boolean glint mask for a given image. Output mask should have 1 for masked, 0 for unmasked."""
+        """Return a boolean glint mask for a given image.
+        Output mask should have 1 for masked, 0 for unmasked.
+        """
         raise NotImplementedError
 
 
@@ -38,10 +40,10 @@ def __init__(self, percent_diffuse: float = 0.95, threshold: float = 0.99):
         Parameters
         ----------
         percent_diffuse
-            An estimate of the percentage of pixels in an image that show pure diffuse reflectance, and thus no specular
-            reflectance (glint).
+            An estimate of the percentage of pixels in an image that show pure diffuse
+            reflectance, and thus no specular reflectance (glint).
         threshold
-            The threshold on the specular reflectance estimate image to convert into a mask.
+            Threshold on specular reflectance estimate to binarize into a mask.
             e.g. if more than 50% specular reflectance is unacceptable, use 0.5.
         """
         super().__init__()
@@ -61,29 +63,34 @@ def __call__(self, img: np.ndarray) -> np.ndarray:
         numpy.ndarray, shape=(H,W)
             Numpy array of glint mask for img at input_path.
         """
-        return self._estimate_specular_reflection_component(img, self.percent_diffuse) > self.threshold
+        return (
+            self._estimate_specular_reflection_component(img, self.percent_diffuse)
+            > self.threshold
+        )
 
     @staticmethod
-    def _estimate_specular_reflection_component(img: np.ndarray, percent_diffuse: float) -> np.ndarray:
+    def _estimate_specular_reflection_component(
+        img: np.ndarray, percent_diffuse: float
+    ) -> np.ndarray:
         """Estimate the specular reflection component of pixels in an image.
 
-            Based on method from:
-                Wang, S., Yu, C., Sun, Y. et al. Specular reflection removal
-                of ocean surface remote sensing images from UAVs. Multimedia Tools
-                Appl 77, 11363–11379 (2018). https://doi.org/10.1007/s11042-017-5551-7
-
-            Parameters
-            ----------
-            img: numpy.ndarray, shape=(H,W,C)
-                A numpy ndarray of an RGB image.
-            percent_diffuse
-                An estimate of the % of pixels that show purely diffuse reflection.
-
-            Returns
-            -------
-            numpy.ndarray, shape=(H,W)
-                An 1D image where values are an estimate of the component of specular reflectance.
-            """
+        Based on method from:
+            Wang, S., Yu, C., Sun, Y. et al. Specular reflection removal
+            of ocean surface remote sensing images from UAVs. Multimedia Tools
+            Appl 77, 11363–11379 (2018). https://doi.org/10.1007/s11042-017-5551-7
+
+        Parameters
+        ----------
+        img: numpy.ndarray, shape=(H,W,C)
+            A numpy ndarray of an RGB image.
+        percent_diffuse
+            An estimate of the % of pixels that show purely diffuse reflection.
+
+        Returns
+        -------
+        numpy.ndarray, shape=(H,W)
+            1D image with values being an estimate of specular reflectance.
+        """
         # Calculate the pixel-wise max intensity and intensity range over RGB channels
         i_max = np.amax(img, axis=2)
         i_min = np.amin(img, axis=2)
@@ -97,7 +104,7 @@ def _estimate_specular_reflection_component(img: np.ndarray, percent_diffuse: fl
         #     specular reflection
         num_thresh = math.ceil(percent_diffuse * q.size)
 
-        # Get intensity ratio for a pixel dividing the image into diffuse and specular sections
+        # Get intensity ratio by separating the image into diffuse and specular sections
         q_x_hat = np.partition(q.ravel(), num_thresh)[num_thresh]
 
         # Estimate the spectral component of each pixel
diff --git a/glint_mask_generator/image_loaders.py b/glint_mask_generator/image_loaders.py
index 653a99c..af4fc63 100644
--- a/glint_mask_generator/image_loaders.py
+++ b/glint_mask_generator/image_loaders.py
@@ -7,18 +7,23 @@
 from abc import ABC, ABCMeta, abstractmethod
 from functools import singledispatchmethod
 from pathlib import Path
-from typing import Iterable, List, Union
+from typing import Iterable, List, Union, TYPE_CHECKING
 
 import numpy as np
 from PIL import Image
 
 from .utils import list_images, normalize_img
 
+if TYPE_CHECKING:
+    from .maskers import Masker
+
 Image.MAX_IMAGE_PIXELS = None
 
 
 class ImageLoader(ABC):
-    def __init__(self, image_directory: str, mask_directory: str):
+    def __init__(
+        self, image_directory: Union[str, Path], mask_directory: Union[str, Path]
+    ):
         super().__init__()
         self.image_directory = Path(image_directory)
         self.mask_directory = Path(mask_directory)
@@ -38,8 +43,8 @@ def paths(self) -> Iterable[Union[List[str], str]]:
 
     @property
     @abstractmethod
-    def _bit_depth(self):
-        return NotImplementedError
+    def _bit_depth(self) -> int:
+        raise NotImplementedError
 
     def preprocess_image(self, img: np.ndarray) -> np.ndarray:
         return normalize_img(img, bit_depth=self._bit_depth)
@@ -64,10 +69,12 @@ def save_mask(mask: np.ndarray, out_path: str):
         out_path
             The path where the file should be saved, including img extension.
         """
-        mask_img = Image.fromarray(mask, mode='L')
+        mask_img = Image.fromarray(mask, mode="L")
         mask_img.save(str(out_path))
 
-    def apply_masker(self, img_paths: Union[List[str], str], masker: "Masker"):  # noqa: F821
+    def apply_masker(
+        self, img_paths: Union[List[str], str], masker: "Masker"
+    ):  # noqa: F821
         img = self.load_image(img_paths)
         img = self.preprocess_image(img)
         mask = masker.algorithm(img)
@@ -82,6 +89,7 @@ class SingleFileImageLoader(ImageLoader, metaclass=ABCMeta):
     @staticmethod
     def load_image(path: str) -> np.ndarray:
         img = Image.open(path)
+        # noinspection PyTypeChecker
         return np.array(img).astype(float)
 
     @property
@@ -97,23 +105,27 @@ class CIRLoader(SingleFileImageLoader):
     _bit_depth = 8
     _crop_size = 256
 
-    def apply_masker(self, img_paths: Union[List[str], str], masker: 'Masker'):  # noqa: F821
-        """ Compute the image mask by moving a window over the input.
-            These CIR images are too large to be read into memory simultaneously so this image masking routine is special.
-
-            This is the setup in the following logic:
-            The inner square is the region to be written to the output.
-            The outer square is a padding section used to ensure the pixel buffers are computed properly.
-            x1,y1
-               ,-----------------,
-               |  x,y            |
-               |   ,--------,    |
-               |   |        |    |
-               |   |        |    |
-               |   |________|    |
-               |          xc,yc  |
-               |_________________|
-                               x2,y2
+    def apply_masker(
+        self, img_paths: Union[List[str], str], masker: "Masker"
+    ):  # noqa: F821
+        """Compute the image mask by moving a window over the input.
+        These CIR images are too large to be read into memory simultaneously so this
+        image masking routine is special.
+
+        This is the setup in the following logic:
+        The inner square is the region to be written to the output.
+        The outer square is a padding section used to ensure the pixel buffers are
+        computed properly.
+        x1,y1
+           ,-----------------,
+           |  x,y            |
+           |   ,--------,    |
+           |   |        |    |
+           |   |        |    |
+           |   |________|    |
+           |          xc,yc  |
+           |_________________|
+                           x2,y2
         """
         if not isinstance(img_paths, str):
             raise RuntimeError("CIRLoader can only operate on images at a single path")
@@ -121,14 +133,14 @@ def apply_masker(self, img_paths: Union[List[str], str], masker: 'Masker'):  # n
             img_path = img_paths
             mask_path = next(self.get_mask_save_paths(img_paths))
 
-        with Image.open(img_path, 'r') as src:
+        with Image.open(img_path, "r") as src:
             height = src.height
             width = src.width
 
             pad = masker.pixel_buffer
 
             # Make an all black image to store the data
-            with Image.new('L', (width, height)) as dest:
+            with Image.new("L", (width, height)) as dest:
                 for y in range(0, height, self._crop_size):
                     for x in range(0, width, self._crop_size):
                         xc = min(x + self._crop_size, width)
@@ -139,6 +151,7 @@ def apply_masker(self, img_paths: Union[List[str], str], masker: 'Masker'):  # n
                         y0 = max(y - pad, 0)
                         y1 = min(yc + pad, height)
 
+                        # noinspection PyTypeChecker
                         img = np.array(src.crop((x0, y0, x1, y1)))
                         img = self.preprocess_image(img)
                         mask = masker.algorithm(img)
@@ -146,8 +159,11 @@ def apply_masker(self, img_paths: Union[List[str], str], masker: 'Masker'):  # n
                         mask = masker.to_metashape_mask(mask)
 
                         # Remove padding
-                        mask = mask[y - y0:, x - x0:]
-                        mask = mask[:self._crop_size + pad - (y1 - yc), :self._crop_size + pad - (x1 - xc)]
+                        mask = mask[(y - y0) :, (x - x0) :]
+                        mask = mask[
+                            : self._crop_size + pad - (y1 - yc),
+                            : self._crop_size + pad - (x1 - xc),
+                        ]
 
                         # Write the mask section
                         dest.paste(Image.fromarray(mask), (x, y))
@@ -156,7 +172,8 @@ def apply_masker(self, img_paths: Union[List[str], str], masker: 'Masker'):  # n
 
 class MultiFileImageLoader(ImageLoader, metaclass=ABCMeta):
     @staticmethod
-    def load_image(paths: List[str]) -> np.ndarray:
+    def load_image(paths: List[Union[str, Path]]) -> np.ndarray:
+        # noinspection PyTypeChecker
         imgs = [np.asarray(Image.open(p)) for p in paths]
         return np.stack(imgs, axis=2).astype(float)
 
@@ -167,10 +184,12 @@ def paths(self) -> Iterable[List[str]]:
 
 
 class MicasenseRedEdgeLoader(MultiFileImageLoader):
-    _blue_band_pattern = re.compile("(.*[\\\\/])?IMG_[0-9]{4}_1.tif", flags=re.IGNORECASE)
+    _blue_band_pattern = re.compile(
+        "(.*[\\\\/])?IMG_[0-9]{4}_1.tif", flags=re.IGNORECASE
+    )
     _bit_depth = 16
 
-    def _is_blue_band_path(self, path: str) -> bool:
+    def _is_blue_band_path(self, path: Union[str, Path]) -> bool:
         return self._blue_band_pattern.match(str(path)) is not None
 
     @property
@@ -180,7 +199,9 @@ def _blue_band_paths(self) -> Iterable[str]:
     @staticmethod
     def _blue_band_path_to_band_paths(path: str) -> List[str]:
         in_path_root = Path(path).stem[:-1]
-        return [str(Path(path).with_name(f"{in_path_root}{i}.tif")) for i in range(1, 6)]
+        return [
+            str(Path(path).with_name(f"{in_path_root}{i}.tif")) for i in range(1, 6)
+        ]
 
     @property
     def paths(self) -> Iterable[List[str]]:
@@ -188,10 +209,12 @@ def paths(self) -> Iterable[List[str]]:
 
 
 class P4MSLoader(MultiFileImageLoader):
-    _blue_band_pattern = re.compile("(.*[\\\\/])?DJI_[0-9]{3}1.TIF", flags=re.IGNORECASE)
+    _blue_band_pattern = re.compile(
+        "(.*[\\\\/])?DJI_[0-9]{3}1.TIF", flags=re.IGNORECASE
+    )
     _bit_depth = 16
 
-    def _is_blue_band_path(self, path: str) -> bool:
+    def _is_blue_band_path(self, path: Union[str, Path]) -> bool:
         return self._blue_band_pattern.match(str(path)) is not None
 
     @property
@@ -199,9 +222,11 @@ def _blue_band_paths(self) -> Iterable[str]:
         return filter(self._is_blue_band_path, list_images(self.image_directory))
 
     @staticmethod
-    def _blue_band_path_to_band_paths(path: str) -> List[str]:
+    def _blue_band_path_to_band_paths(path: Union[str, Path]) -> List[str]:
         in_path_root = Path(path).stem[:-1]
-        return [str(Path(path).with_name(f"{in_path_root}{i}.TIF")) for i in range(1, 6)]
+        return [
+            str(Path(path).with_name(f"{in_path_root}{i}.TIF")) for i in range(1, 6)
+        ]
 
     @property
     def paths(self) -> Iterable[List[str]]:
diff --git a/glint_mask_generator/maskers.py b/glint_mask_generator/maskers.py
index c6b10ef..3483624 100644
--- a/glint_mask_generator/maskers.py
+++ b/glint_mask_generator/maskers.py
@@ -11,12 +11,23 @@
 from scipy.ndimage import convolve
 
 from .glint_algorithms import GlintAlgorithm, ThresholdAlgorithm
-from .image_loaders import CIRLoader, ImageLoader, MicasenseRedEdgeLoader, P4MSLoader, RGBLoader
+from .image_loaders import (
+    CIRLoader,
+    ImageLoader,
+    MicasenseRedEdgeLoader,
+    P4MSLoader,
+    RGBLoader,
+)
 from .utils import make_circular_kernel
 
 
 class Masker(object):
-    def __init__(self, algorithm: GlintAlgorithm, image_loader: ImageLoader, pixel_buffer: int = 0):
+    def __init__(
+        self,
+        algorithm: GlintAlgorithm,
+        image_loader: ImageLoader,
+        pixel_buffer: int = 0,
+    ):
         self.algorithm = algorithm
         self.image_loader = image_loader
         self.pixel_buffer = pixel_buffer
@@ -24,10 +35,12 @@ def __init__(self, algorithm: GlintAlgorithm, image_loader: ImageLoader, pixel_b
 
     # noinspection PyMethodMayBeStatic
     def postprocess_mask(self, mask: np.ndarray) -> np.ndarray:
-        """Method which can be overridden to do any postprocessing on the generated boolean numpy mask."""
+        """Hook to do any postprocessing on the generated boolean numpy mask."""
         if self.pixel_buffer <= 0:
             return mask
-        return (convolve(mask, self.buffer_kernel, mode='constant', cval=0) > 0).astype(int)
+        return (convolve(mask, self.buffer_kernel, mode="constant", cval=0) > 0).astype(
+            int
+        )
 
     @staticmethod
     def to_metashape_mask(mask: np.ndarray):
@@ -43,20 +56,25 @@ def __len__(self) -> int:
         """
         return len(self.image_loader)
 
-    def __call__(self, max_workers: int,
-                 callback: Optional[Callable[[str], None]] = None,
-                 err_callback: Optional[Callable[[str, Exception], None]] = None
-                 ) -> None:
-        """Allows calling self.process by calling the object as a function.
+    def __call__(
+        self,
+        max_workers: int,
+        callback: Optional[Callable[[str], None]] = None,
+        err_callback: Optional[Callable[[str, Exception], None]] = None,
+    ) -> None:
+        """Calling Masker.process by calling the instance as a function.
 
         Parameters
         ----------
         max_workers
-            The number of threads to use during processing. Useful for limiting memory consumption.
+            The number of threads to use during processing. Useful for limiting memory
+            consumption.
         callback
-            Optional callback that receives the img_path as an arg after it is successfully processed.
+            Optional callback that receives the img_path as an arg after it is
+            successfully processed.
         err_callback
-            Optional callback that receives the img_path and an Exception as args after a processing failure.
+            Optional callback that receives the img_path and an Exception as args after
+            a processing failure.
         """
         if max_workers == 0:
             return self.process_unthreaded(callback, err_callback)
@@ -64,8 +82,11 @@ def __call__(self, max_workers: int,
             return self.process(max_workers, callback, err_callback)
 
     # noinspection SpellCheckingInspection
-    def process_unthreaded(self, callback: Optional[Callable[[List[str]], None]] = None,
-                           err_callback: Optional[Callable[[List[str], Exception], None]] = None) -> None:
+    def process_unthreaded(
+        self,
+        callback: Optional[Callable[[List[str]], None]] = None,
+        err_callback: Optional[Callable[[List[str], Exception], None]] = None,
+    ) -> None:
         for paths in self.image_loader.paths:
             try:
                 self._process_one(paths)
@@ -77,11 +98,16 @@ def process_unthreaded(self, callback: Optional[Callable[[List[str]], None]] = N
                     err_callback(paths, exc)
                 return
 
-    def process(self, max_workers: int = os.cpu_count(), callback: Optional[Callable[[List[str]], None]] = None,
-                err_callback: Optional[Callable[[List[str], Exception], None]] = None) -> None:
+    def process(
+        self,
+        max_workers: int = os.cpu_count(),
+        callback: Optional[Callable[[List[str]], None]] = None,
+        err_callback: Optional[Callable[[List[str], Exception], None]] = None,
+    ) -> None:
         """Compute all glint masks.
 
-        Computes masks for all images in self.img_paths using the process_func and save to the mask_out_paths.
+        Computes masks for all images in self.img_paths using the process_func and save
+        to the mask_out_paths.
 
         Parameters
         ----------
@@ -89,8 +115,8 @@ def process(self, max_workers: int = os.cpu_count(), callback: Optional[Callable
             The maximum number of image processing workers.
             Useful for limiting memory usage.
         callback
-            Optional callback function passed the name of each input and output mask files after processing it.
-            Will receive img_path: str as arg.
+            Optional callback function passed the name of each input and output mask
+            files after processing it. Will receive img_path: str as arg.
         err_callback
             Optional callback function passed exception object on processing failure.
             Will receive img_path: str, and the Exception as args.
@@ -116,46 +142,77 @@ def process(self, max_workers: int = os.cpu_count(), callback: Optional[Callable
     def _process_one(self, paths: Union[List[str], str]) -> None:
         """Generates and saves a glint mask for the image located at img_path.
 
-        Saves the generated mask to all path locations returned by self.get_mask_save_paths(img_path).
+        Saves the generated mask to all path locations returned by
+        self.get_mask_save_paths(img_path).
 
         Parameters
         ----------
         paths
-            The file paths used to create the image. Can be single file path or list of path to multiple files
+            The file paths used to create the image. Can be single file path or list of
+            path to multiple files
         """
         self.image_loader.apply_masker(paths, self)
 
 
 class RGBThresholdMasker(Masker):
-    def __init__(self, img_dir: str, mask_dir: str,
-                 thresholds: Sequence[float] = (1, 1, 0.875), pixel_buffer: int = 0):
-        super().__init__(algorithm=ThresholdAlgorithm(thresholds),
-                         image_loader=RGBLoader(img_dir, mask_dir),
-                         pixel_buffer=pixel_buffer)
+    def __init__(
+        self,
+        img_dir: str,
+        mask_dir: str,
+        thresholds: Sequence[float] = (1, 1, 0.875),
+        pixel_buffer: int = 0,
+    ):
+        super().__init__(
+            algorithm=ThresholdAlgorithm(thresholds),
+            image_loader=RGBLoader(img_dir, mask_dir),
+            pixel_buffer=pixel_buffer,
+        )
 
 
 class CIRThresholdMasker(Masker):
-    def __init__(self, img_dir: str, mask_dir: str,
-                 thresholds: Sequence[float] = (1, 1, 0.875, 1), pixel_buffer: int = 0):
-        super().__init__(algorithm=ThresholdAlgorithm(thresholds),
-                         image_loader=CIRLoader(img_dir, mask_dir),
-                         pixel_buffer=pixel_buffer)
+    def __init__(
+        self,
+        img_dir: str,
+        mask_dir: str,
+        thresholds: Sequence[float] = (1, 1, 0.875, 1),
+        pixel_buffer: int = 0,
+    ):
+        super().__init__(
+            algorithm=ThresholdAlgorithm(thresholds),
+            image_loader=CIRLoader(img_dir, mask_dir),
+            pixel_buffer=pixel_buffer,
+        )
 
 
 class P4MSThresholdMasker(Masker):
-    def __init__(self, img_dir: str, mask_dir: str,
-                 thresholds: Sequence[float] = (0.875, 1, 1, 1, 1), pixel_buffer: int = 0):
-        super().__init__(algorithm=ThresholdAlgorithm(thresholds),
-                         image_loader=P4MSLoader(img_dir, mask_dir),
-                         pixel_buffer=pixel_buffer)
+    def __init__(
+        self,
+        img_dir: str,
+        mask_dir: str,
+        thresholds: Sequence[float] = (0.875, 1, 1, 1, 1),
+        pixel_buffer: int = 0,
+    ):
+        super().__init__(
+            algorithm=ThresholdAlgorithm(thresholds),
+            image_loader=P4MSLoader(img_dir, mask_dir),
+            pixel_buffer=pixel_buffer,
+        )
 
 
 class MicasenseRedEdgeThresholdMasker(Masker):
-    def __init__(self, img_dir: str, mask_dir: str,
-                 thresholds: Sequence[float] = (0.875, 1, 1, 1, 1), pixel_buffer: int = 0):
-        super().__init__(algorithm=ThresholdAlgorithm(thresholds),
-                         image_loader=MicasenseRedEdgeLoader(img_dir, mask_dir),
-                         pixel_buffer=pixel_buffer)
-
-if __name__ == '__main__':
+    def __init__(
+        self,
+        img_dir: str,
+        mask_dir: str,
+        thresholds: Sequence[float] = (0.875, 1, 1, 1, 1),
+        pixel_buffer: int = 0,
+    ):
+        super().__init__(
+            algorithm=ThresholdAlgorithm(thresholds),
+            image_loader=MicasenseRedEdgeLoader(img_dir, mask_dir),
+            pixel_buffer=pixel_buffer,
+        )
+
+
+if __name__ == "__main__":
     CIRThresholdMasker("ignore/in", "ignore/out", pixel_buffer=10).process_unthreaded()
diff --git a/glint_mask_generator/utils.py b/glint_mask_generator/utils.py
index 4a28b86..3eb981a 100644
--- a/glint_mask_generator/utils.py
+++ b/glint_mask_generator/utils.py
@@ -10,7 +10,7 @@
 
 
 def normalize_img(img: np.ndarray, bit_depth: int) -> np.ndarray:
-    """Utility function to normalize the values of an image with arbitrary bit_depth to range [0, 1].
+    """Normalize the values of an image with arbitrary bit_depth to range [0, 1].
 
     Parameters
     ----------
@@ -31,9 +31,9 @@ def normalize_img(img: np.ndarray, bit_depth: int) -> np.ndarray:
 def list_images(img_dir) -> Iterable[str]:
     """List all image files in img_dir.
 
-    Returns an iterator that lists the files to process. Subclasses may want to override this to return specific
-    image types or filter the results. By default, will list all images in self.img_dir if the file extension is in
-    the extensions list.
+    Returns an iterator that lists the files to process. Subclasses may want to override
+    this to return specific image types or filter the results. By default, will list all
+    images in self.img_dir if the file extension is in the extensions list.
 
     Returns
     -------
@@ -49,6 +49,6 @@ def list_images(img_dir) -> Iterable[str]:
 
 def make_circular_kernel(size: int) -> np.ndarray:
     """Create circular kernel"""
-    y, x = np.ogrid[-size:size + 1, -size:size + 1]
-    dist_m: np.ndarray = x ** 2 + y ** 2
-    return dist_m <= size ** 2
+    y, x = np.ogrid[-size : size + 1, -size : size + 1]
+    dist_m: np.ndarray = x**2 + y**2
+    return dist_m <= size**2
diff --git a/gui/__main__.py b/gui/__main__.py
index aaa4c56..342ea09 100644
--- a/gui/__main__.py
+++ b/gui/__main__.py
@@ -12,12 +12,17 @@
 from PyQt6.QtGui import QIcon
 from loguru import logger
 
-from glint_mask_generator import CIRThresholdMasker, Masker, MicasenseRedEdgeThresholdMasker, P4MSThresholdMasker, \
-    RGBThresholdMasker
+from glint_mask_generator import (
+    CIRThresholdMasker,
+    Masker,
+    MicasenseRedEdgeThresholdMasker,
+    P4MSThresholdMasker,
+    RGBThresholdMasker,
+)
 from gui import __version__
 from gui.utils import resource_path
 
-# String constants reduce occurrence of silent errors due to typos when doing comparisons
+# String constants to eliminate silent errors due to typos
 BLUE = "BLUE"
 GREEN = "GREEN"
 RED = "RED"
@@ -63,7 +68,7 @@ def __init__(self, parent):
 class GlintMaskGenerator(QtWidgets.QMainWindow):
     def __init__(self):
         super().__init__()
-        uic.loadUi(resource_path('resources/gui.ui'), self)
+        uic.loadUi(resource_path("resources/gui.ui"), self)
 
         # Setup window
         self.setWindowTitle(f"Glint Mask Generator v{__version__}")
@@ -85,7 +90,9 @@ def __init__(self):
 
         # Run non-ui jobs in a separate thread
         self.threadpool = QThreadPool()
-        print("Multithreading with maximum %d threads" % self.threadpool.maxThreadCount())
+        print(
+            "Multithreading with maximum %d threads" % self.threadpool.maxThreadCount()
+        )
 
         # Set max workers to good default
         self.max_workers = os.cpu_count()
@@ -135,7 +142,9 @@ def max_workers(self, v):
 
     @property
     def band_order_ints(self) -> Sequence[int]:
-        return [{BLUE: 0, GREEN: 1, RED: 2, REDEDGE: 3, NIR: 4}[k] for k in self.band_order]
+        return [
+            {BLUE: 0, GREEN: 1, RED: 2, REDEDGE: 3, NIR: 4}[k] for k in self.band_order
+        ]
 
     @property
     def band_order(self) -> Sequence[str]:
@@ -150,7 +159,10 @@ def band_order(self) -> Sequence[str]:
 
     @property
     def threshold_values(self) -> Sequence[float]:
-        """Returns the thresholds in the order corresponding to the imagery type band order."""
+        """
+        Returns the thresholds in the order corresponding to the imagery type band
+        order.
+        """
         thresholds: List[float] = [
             self.blue_thresh_w.value,
             self.green_thresh_w.value,
@@ -161,12 +173,15 @@ def threshold_values(self) -> Sequence[float]:
         return [thresholds[i] for i in self.band_order_ints]
 
     def create_masker(self) -> Masker:
-        """Returns an instance of the appropriate glint mask generator given selected options."""
+        """
+        Returns an instance of the appropriate glint mask generator given selected
+        options.
+        """
         threshold_params = dict(
             img_dir=self.img_dir_w.value,
             mask_dir=self.mask_dir_w.value,
             thresholds=self.threshold_values,
-            pixel_buffer=self.pixel_buffer_w.value
+            pixel_buffer=self.pixel_buffer_w.value,
         )
 
         if self.img_type == IMG_TYPE_RGB:
@@ -200,10 +215,10 @@ def _inc_progress(self, _):
         self.progress_val += 1
 
         if self.progress_val == self.progress_maximum:
-            self.info_msg.show_message('Processing is complete')
+            self.info_msg.show_message("Processing is complete")
 
     def _err_callback(self, img_path, err):
-        msg = '%r generated an exception: %s' % (img_path, err)
+        msg = "%r generated an exception: %s" % (img_path, err)
         self.err_msg.show_message(msg)
 
     @logger.catch
@@ -242,8 +257,8 @@ def __init__(self, fn, *args, **kwargs):
         self.signals = Signals()
 
         # Add callbacks to kwargs
-        self.kwargs['callback'] = self.signals.progress.emit
-        self.kwargs['err_callback'] = self.signals.error.emit
+        self.kwargs["callback"] = self.signals.progress.emit
+        self.kwargs["err_callback"] = self.signals.error.emit
 
     @pyqtSlot()
     def run(self):
@@ -251,7 +266,7 @@ def run(self):
         self.signals.finished.emit()
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     app = QtWidgets.QApplication(sys.argv)
     main_window = GlintMaskGenerator()
     app.exec()
diff --git a/gui/utils.py b/gui/utils.py
index 2bfd49e..56a8bff 100644
--- a/gui/utils.py
+++ b/gui/utils.py
@@ -3,6 +3,6 @@
 
 
 def resource_path(relative_path):
-    """ Get absolute path to resource, works for dev and for PyInstaller """
-    base_path = getattr(sys, '_MEIPASS', path.dirname(__file__))
+    """Get absolute path to resource, works for dev and for PyInstaller"""
+    base_path = getattr(sys, "_MEIPASS", path.dirname(__file__))
     return path.abspath(path.join(base_path, relative_path))
diff --git a/gui/widgets/buffer_ctrl.py b/gui/widgets/buffer_ctrl.py
index 6317f91..2ce405c 100644
--- a/gui/widgets/buffer_ctrl.py
+++ b/gui/widgets/buffer_ctrl.py
@@ -13,10 +13,14 @@ class BufferCtrl(QtWidgets.QWidget):
     def __init__(self, parent):
         super().__init__(parent)
 
-        uic.loadUi(resource_path('resources/buffer_ctrl.ui'), self)
-
-        self.slider.valueChanged.connect(lambda value: self.spinbox.setValue(int(value)))
-        self.spinbox.valueChanged.connect(lambda value: self.slider.setValue(int(value)))
+        uic.loadUi(resource_path("resources/buffer_ctrl.ui"), self)
+
+        self.slider.valueChanged.connect(
+            lambda value: self.spinbox.setValue(int(value))
+        )
+        self.spinbox.valueChanged.connect(
+            lambda value: self.slider.setValue(int(value))
+        )
 
         self.show()
 
diff --git a/gui/widgets/directory_path.py b/gui/widgets/directory_path.py
index e290a46..b954c4c 100644
--- a/gui/widgets/directory_path.py
+++ b/gui/widgets/directory_path.py
@@ -14,7 +14,7 @@ class DirectoryPath(QtWidgets.QWidget):
     def __init__(self, parent):
         super().__init__(parent)
 
-        uic.loadUi(resource_path('resources/directory_path.ui'), self)
+        uic.loadUi(resource_path("resources/directory_path.ui"), self)
         self.show()
 
     @property
@@ -27,4 +27,5 @@ def value(self, path: str):
 
     def dir_btn_clicked(self):
         self.value = QFileDialog.getExistingDirectory(
-            self, 'Select directory', self.value, QFileDialog.Option.ShowDirsOnly)
+            self, "Select directory", self.value, QFileDialog.Option.ShowDirsOnly
+        )
diff --git a/gui/widgets/threshold_ctrl.py b/gui/widgets/threshold_ctrl.py
index bb4296a..624d883 100644
--- a/gui/widgets/threshold_ctrl.py
+++ b/gui/widgets/threshold_ctrl.py
@@ -13,10 +13,14 @@ class ThresholdCtrl(QtWidgets.QWidget):
     def __init__(self, parent):
         super().__init__(parent)
 
-        uic.loadUi(resource_path('resources/threshold_ctrl.ui'), self)
-
-        self.slider.valueChanged.connect(lambda value: self.spinbox.setValue(value / 1000.))
-        self.spinbox.valueChanged.connect(lambda value: self.slider.setValue(int(value * 1000)))
+        uic.loadUi(resource_path("resources/threshold_ctrl.ui"), self)
+
+        self.slider.valueChanged.connect(
+            lambda value: self.spinbox.setValue(value / 1000.0)
+        )
+        self.spinbox.valueChanged.connect(
+            lambda value: self.slider.setValue(int(value * 1000))
+        )
 
         self.show()
 
diff --git a/poetry.lock b/poetry.lock
index 0e4181b..edb835a 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,9 +1,10 @@
-# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand.
+# This file is automatically @generated by Poetry and should not be changed by hand.
 
 [[package]]
 name = "altgraph"
 version = "0.17.3"
 description = "Python graph (network) package"
+category = "dev"
 optional = false
 python-versions = "*"
 files = [
@@ -12,26 +13,84 @@ files = [
 ]
 
 [[package]]
-name = "attrs"
-version = "22.1.0"
-description = "Classes Without Boilerplate"
+name = "black"
+version = "23.7.0"
+description = "The uncompromising code formatter."
+category = "dev"
 optional = false
-python-versions = ">=3.5"
+python-versions = ">=3.8"
 files = [
-    {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"},
-    {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"},
+    {file = "black-23.7.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:5c4bc552ab52f6c1c506ccae05681fab58c3f72d59ae6e6639e8885e94fe2587"},
+    {file = "black-23.7.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:552513d5cd5694590d7ef6f46e1767a4df9af168d449ff767b13b084c020e63f"},
+    {file = "black-23.7.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:86cee259349b4448adb4ef9b204bb4467aae74a386bce85d56ba4f5dc0da27be"},
+    {file = "black-23.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:501387a9edcb75d7ae8a4412bb8749900386eaef258f1aefab18adddea1936bc"},
+    {file = "black-23.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb074d8b213749fa1d077d630db0d5f8cc3b2ae63587ad4116e8a436e9bbe995"},
+    {file = "black-23.7.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:b5b0ee6d96b345a8b420100b7d71ebfdd19fab5e8301aff48ec270042cd40ac2"},
+    {file = "black-23.7.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:893695a76b140881531062d48476ebe4a48f5d1e9388177e175d76234ca247cd"},
+    {file = "black-23.7.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:c333286dc3ddca6fdff74670b911cccedacb4ef0a60b34e491b8a67c833b343a"},
+    {file = "black-23.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831d8f54c3a8c8cf55f64d0422ee875eecac26f5f649fb6c1df65316b67c8926"},
+    {file = "black-23.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:7f3bf2dec7d541b4619b8ce526bda74a6b0bffc480a163fed32eb8b3c9aed8ad"},
+    {file = "black-23.7.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:f9062af71c59c004cd519e2fb8f5d25d39e46d3af011b41ab43b9c74e27e236f"},
+    {file = "black-23.7.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:01ede61aac8c154b55f35301fac3e730baf0c9cf8120f65a9cd61a81cfb4a0c3"},
+    {file = "black-23.7.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:327a8c2550ddc573b51e2c352adb88143464bb9d92c10416feb86b0f5aee5ff6"},
+    {file = "black-23.7.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1c6022b86f83b632d06f2b02774134def5d4d4f1dac8bef16d90cda18ba28a"},
+    {file = "black-23.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:27eb7a0c71604d5de083757fbdb245b1a4fae60e9596514c6ec497eb63f95320"},
+    {file = "black-23.7.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:8417dbd2f57b5701492cd46edcecc4f9208dc75529bcf76c514864e48da867d9"},
+    {file = "black-23.7.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:47e56d83aad53ca140da0af87678fb38e44fd6bc0af71eebab2d1f59b1acf1d3"},
+    {file = "black-23.7.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:25cc308838fe71f7065df53aedd20327969d05671bac95b38fdf37ebe70ac087"},
+    {file = "black-23.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:642496b675095d423f9b8448243336f8ec71c9d4d57ec17bf795b67f08132a91"},
+    {file = "black-23.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:ad0014efc7acf0bd745792bd0d8857413652979200ab924fbf239062adc12491"},
+    {file = "black-23.7.0-py3-none-any.whl", hash = "sha256:9fd59d418c60c0348505f2ddf9609c1e1de8e7493eab96198fc89d9f865e7a96"},
+    {file = "black-23.7.0.tar.gz", hash = "sha256:022a582720b0d9480ed82576c920a8c1dde97cc38ff11d8d8859b3bd6ca9eedb"},
 ]
 
+[package.dependencies]
+click = ">=8.0.0"
+mypy-extensions = ">=0.4.3"
+packaging = ">=22.0"
+pathspec = ">=0.9.0"
+platformdirs = ">=2"
+tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
+typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}
+
 [package.extras]
-dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"]
-docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"]
-tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"]
-tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"]
+colorama = ["colorama (>=0.4.3)"]
+d = ["aiohttp (>=3.7.4)"]
+jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
+uvloop = ["uvloop (>=0.15.2)"]
+
+[[package]]
+name = "cfgv"
+version = "3.3.1"
+description = "Validate configuration and produce human readable error messages."
+category = "dev"
+optional = false
+python-versions = ">=3.6.1"
+files = [
+    {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"},
+    {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"},
+]
+
+[[package]]
+name = "click"
+version = "8.1.6"
+description = "Composable command line interface toolkit"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "click-8.1.6-py3-none-any.whl", hash = "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"},
+    {file = "click-8.1.6.tar.gz", hash = "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "platform_system == \"Windows\""}
 
 [[package]]
 name = "colorama"
 version = "0.4.6"
 description = "Cross-platform colored terminal text."
+category = "main"
 optional = false
 python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
 files = [
@@ -39,28 +98,58 @@ files = [
     {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
 ]
 
+[[package]]
+name = "distlib"
+version = "0.3.7"
+description = "Distribution utilities"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+    {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"},
+    {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"},
+]
+
 [[package]]
 name = "exceptiongroup"
-version = "1.0.4"
+version = "1.1.2"
 description = "Backport of PEP 654 (exception groups)"
+category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "exceptiongroup-1.0.4-py3-none-any.whl", hash = "sha256:542adf9dea4055530d6e1279602fa5cb11dab2395fa650b8674eaec35fc4a828"},
-    {file = "exceptiongroup-1.0.4.tar.gz", hash = "sha256:bd14967b79cd9bdb54d97323216f8fdf533e278df937aa2a90089e7d6e06e5ec"},
+    {file = "exceptiongroup-1.1.2-py3-none-any.whl", hash = "sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f"},
+    {file = "exceptiongroup-1.1.2.tar.gz", hash = "sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5"},
 ]
 
 [package.extras]
 test = ["pytest (>=6)"]
 
+[[package]]
+name = "filelock"
+version = "3.12.2"
+description = "A platform independent file lock."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "filelock-3.12.2-py3-none-any.whl", hash = "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec"},
+    {file = "filelock-3.12.2.tar.gz", hash = "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81"},
+]
+
+[package.extras]
+docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"]
+testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"]
+
 [[package]]
 name = "fire"
-version = "0.4.0"
+version = "0.5.0"
 description = "A library for automatically generating command line interfaces."
+category = "main"
 optional = false
 python-versions = "*"
 files = [
-    {file = "fire-0.4.0.tar.gz", hash = "sha256:c5e2b8763699d1142393a46d0e3e790c5eb2f0706082df8f647878842c216a62"},
+    {file = "fire-0.5.0.tar.gz", hash = "sha256:a6b0d49e98c8963910021f92bba66f65ab440da2982b78eb1bbf95a0a34aacc6"},
 ]
 
 [package.dependencies]
@@ -71,6 +160,7 @@ termcolor = "*"
 name = "flake8"
 version = "6.0.0"
 description = "the modular source code checker: pep8 pyflakes and co"
+category = "dev"
 optional = false
 python-versions = ">=3.8.1"
 files = [
@@ -84,35 +174,42 @@ pycodestyle = ">=2.10.0,<2.11.0"
 pyflakes = ">=3.0.0,<3.1.0"
 
 [[package]]
-name = "future"
-version = "0.18.3"
-description = "Clean single-source support for Python 3 and 2"
+name = "identify"
+version = "2.5.26"
+description = "File identification library for Python"
+category = "dev"
 optional = false
-python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+python-versions = ">=3.8"
 files = [
-    {file = "future-0.18.3.tar.gz", hash = "sha256:34a17436ed1e96697a86f9de3d15a3b0be01d8bc8de9c1dffd59fb8234ed5307"},
+    {file = "identify-2.5.26-py2.py3-none-any.whl", hash = "sha256:c22a8ead0d4ca11f1edd6c9418c3220669b3b7533ada0a0ffa6cc0ef85cf9b54"},
+    {file = "identify-2.5.26.tar.gz", hash = "sha256:7243800bce2f58404ed41b7c002e53d4d22bcf3ae1b7900c2d7aefd95394bf7f"},
 ]
 
+[package.extras]
+license = ["ukkonen"]
+
 [[package]]
 name = "iniconfig"
-version = "1.1.1"
-description = "iniconfig: brain-dead simple config-ini parsing"
+version = "2.0.0"
+description = "brain-dead simple config-ini parsing"
+category = "dev"
 optional = false
-python-versions = "*"
+python-versions = ">=3.7"
 files = [
-    {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
-    {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
+    {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
+    {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
 ]
 
 [[package]]
 name = "loguru"
-version = "0.6.0"
+version = "0.7.0"
 description = "Python logging made (stupidly) simple"
+category = "main"
 optional = false
 python-versions = ">=3.5"
 files = [
-    {file = "loguru-0.6.0-py3-none-any.whl", hash = "sha256:4e2414d534a2ab57573365b3e6d0234dfb1d84b68b7f3b948e6fb743860a77c3"},
-    {file = "loguru-0.6.0.tar.gz", hash = "sha256:066bd06758d0a513e9836fd9c6b5a75bfb3fd36841f4b996bc60b547a309d41c"},
+    {file = "loguru-0.7.0-py3-none-any.whl", hash = "sha256:b93aa30099fa6860d4727f1b81f8718e965bb96253fa190fab2077aaad6d15d3"},
+    {file = "loguru-0.7.0.tar.gz", hash = "sha256:1612053ced6ae84d7959dd7d5e431a0532642237ec21f7fd83ac73fe539e03e1"},
 ]
 
 [package.dependencies]
@@ -120,12 +217,13 @@ colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""}
 win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""}
 
 [package.extras]
-dev = ["Sphinx (>=4.1.1)", "black (>=19.10b0)", "colorama (>=0.3.4)", "docutils (==0.16)", "flake8 (>=3.7.7)", "isort (>=5.1.1)", "pytest (>=4.6.2)", "pytest-cov (>=2.7.1)", "sphinx-autobuild (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "tox (>=3.9.0)"]
+dev = ["Sphinx (==5.3.0)", "colorama (==0.4.5)", "colorama (==0.4.6)", "freezegun (==1.1.0)", "freezegun (==1.2.2)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v0.990)", "pre-commit (==3.2.1)", "pytest (==6.1.2)", "pytest (==7.2.1)", "pytest-cov (==2.12.1)", "pytest-cov (==4.0.0)", "pytest-mypy-plugins (==1.10.1)", "pytest-mypy-plugins (==1.9.3)", "sphinx-autobuild (==2021.3.14)", "sphinx-rtd-theme (==1.2.0)", "tox (==3.27.1)", "tox (==4.4.6)"]
 
 [[package]]
 name = "macholib"
 version = "1.16.2"
 description = "Mach-O header analysis and editing"
+category = "dev"
 optional = false
 python-versions = "*"
 files = [
@@ -140,6 +238,7 @@ altgraph = ">=0.17"
 name = "mccabe"
 version = "0.7.0"
 description = "McCabe checker, plugin for flake8"
+category = "dev"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -148,73 +247,108 @@ files = [
 ]
 
 [[package]]
-name = "numpy"
-version = "1.23.5"
-description = "NumPy is the fundamental package for array computing with Python."
+name = "mypy-extensions"
+version = "1.0.0"
+description = "Type system extensions for programs checked with the mypy type checker."
+category = "dev"
 optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.5"
+files = [
+    {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
+    {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
+]
+
+[[package]]
+name = "nodeenv"
+version = "1.8.0"
+description = "Node.js virtual environment builder"
+category = "dev"
+optional = false
+python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
 files = [
-    {file = "numpy-1.23.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9c88793f78fca17da0145455f0d7826bcb9f37da4764af27ac945488116efe63"},
-    {file = "numpy-1.23.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e9f4c4e51567b616be64e05d517c79a8a22f3606499941d97bb76f2ca59f982d"},
-    {file = "numpy-1.23.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7903ba8ab592b82014713c491f6c5d3a1cde5b4a3bf116404e08f5b52f6daf43"},
-    {file = "numpy-1.23.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e05b1c973a9f858c74367553e236f287e749465f773328c8ef31abe18f691e1"},
-    {file = "numpy-1.23.5-cp310-cp310-win32.whl", hash = "sha256:522e26bbf6377e4d76403826ed689c295b0b238f46c28a7251ab94716da0b280"},
-    {file = "numpy-1.23.5-cp310-cp310-win_amd64.whl", hash = "sha256:dbee87b469018961d1ad79b1a5d50c0ae850000b639bcb1b694e9981083243b6"},
-    {file = "numpy-1.23.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ce571367b6dfe60af04e04a1834ca2dc5f46004ac1cc756fb95319f64c095a96"},
-    {file = "numpy-1.23.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56e454c7833e94ec9769fa0f86e6ff8e42ee38ce0ce1fa4cbb747ea7e06d56aa"},
-    {file = "numpy-1.23.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5039f55555e1eab31124a5768898c9e22c25a65c1e0037f4d7c495a45778c9f2"},
-    {file = "numpy-1.23.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58f545efd1108e647604a1b5aa809591ccd2540f468a880bedb97247e72db387"},
-    {file = "numpy-1.23.5-cp311-cp311-win32.whl", hash = "sha256:b2a9ab7c279c91974f756c84c365a669a887efa287365a8e2c418f8b3ba73fb0"},
-    {file = "numpy-1.23.5-cp311-cp311-win_amd64.whl", hash = "sha256:0cbe9848fad08baf71de1a39e12d1b6310f1d5b2d0ea4de051058e6e1076852d"},
-    {file = "numpy-1.23.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f063b69b090c9d918f9df0a12116029e274daf0181df392839661c4c7ec9018a"},
-    {file = "numpy-1.23.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0aaee12d8883552fadfc41e96b4c82ee7d794949e2a7c3b3a7201e968c7ecab9"},
-    {file = "numpy-1.23.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92c8c1e89a1f5028a4c6d9e3ccbe311b6ba53694811269b992c0b224269e2398"},
-    {file = "numpy-1.23.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d208a0f8729f3fb790ed18a003f3a57895b989b40ea4dce4717e9cf4af62c6bb"},
-    {file = "numpy-1.23.5-cp38-cp38-win32.whl", hash = "sha256:06005a2ef6014e9956c09ba07654f9837d9e26696a0470e42beedadb78c11b07"},
-    {file = "numpy-1.23.5-cp38-cp38-win_amd64.whl", hash = "sha256:ca51fcfcc5f9354c45f400059e88bc09215fb71a48d3768fb80e357f3b457e1e"},
-    {file = "numpy-1.23.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8969bfd28e85c81f3f94eb4a66bc2cf1dbdc5c18efc320af34bffc54d6b1e38f"},
-    {file = "numpy-1.23.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a7ac231a08bb37f852849bbb387a20a57574a97cfc7b6cabb488a4fc8be176de"},
-    {file = "numpy-1.23.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf837dc63ba5c06dc8797c398db1e223a466c7ece27a1f7b5232ba3466aafe3d"},
-    {file = "numpy-1.23.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33161613d2269025873025b33e879825ec7b1d831317e68f4f2f0f84ed14c719"},
-    {file = "numpy-1.23.5-cp39-cp39-win32.whl", hash = "sha256:af1da88f6bc3d2338ebbf0e22fe487821ea4d8e89053e25fa59d1d79786e7481"},
-    {file = "numpy-1.23.5-cp39-cp39-win_amd64.whl", hash = "sha256:09b7847f7e83ca37c6e627682f145856de331049013853f344f37b0c9690e3df"},
-    {file = "numpy-1.23.5-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:abdde9f795cf292fb9651ed48185503a2ff29be87770c3b8e2a14b0cd7aa16f8"},
-    {file = "numpy-1.23.5-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9a909a8bae284d46bbfdefbdd4a262ba19d3bc9921b1e76126b1d21c3c34135"},
-    {file = "numpy-1.23.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:01dd17cbb340bf0fc23981e52e1d18a9d4050792e8fb8363cecbf066a84b827d"},
-    {file = "numpy-1.23.5.tar.gz", hash = "sha256:1b1766d6f397c18153d40015ddfc79ddb715cabadc04d2d228d4e5a8bc4ded1a"},
+    {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"},
+    {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"},
+]
+
+[package.dependencies]
+setuptools = "*"
+
+[[package]]
+name = "numpy"
+version = "1.25.1"
+description = "Fundamental package for array computing in Python"
+category = "main"
+optional = false
+python-versions = ">=3.9"
+files = [
+    {file = "numpy-1.25.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:77d339465dff3eb33c701430bcb9c325b60354698340229e1dff97745e6b3efa"},
+    {file = "numpy-1.25.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d736b75c3f2cb96843a5c7f8d8ccc414768d34b0a75f466c05f3a739b406f10b"},
+    {file = "numpy-1.25.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a90725800caeaa160732d6b31f3f843ebd45d6b5f3eec9e8cc287e30f2805bf"},
+    {file = "numpy-1.25.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c6c9261d21e617c6dc5eacba35cb68ec36bb72adcff0dee63f8fbc899362588"},
+    {file = "numpy-1.25.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0def91f8af6ec4bb94c370e38c575855bf1d0be8a8fbfba42ef9c073faf2cf19"},
+    {file = "numpy-1.25.1-cp310-cp310-win32.whl", hash = "sha256:fd67b306320dcadea700a8f79b9e671e607f8696e98ec255915c0c6d6b818503"},
+    {file = "numpy-1.25.1-cp310-cp310-win_amd64.whl", hash = "sha256:c1516db588987450b85595586605742879e50dcce923e8973f79529651545b57"},
+    {file = "numpy-1.25.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6b82655dd8efeea69dbf85d00fca40013d7f503212bc5259056244961268b66e"},
+    {file = "numpy-1.25.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e8f6049c4878cb16960fbbfb22105e49d13d752d4d8371b55110941fb3b17800"},
+    {file = "numpy-1.25.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41a56b70e8139884eccb2f733c2f7378af06c82304959e174f8e7370af112e09"},
+    {file = "numpy-1.25.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5154b1a25ec796b1aee12ac1b22f414f94752c5f94832f14d8d6c9ac40bcca6"},
+    {file = "numpy-1.25.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:38eb6548bb91c421261b4805dc44def9ca1a6eef6444ce35ad1669c0f1a3fc5d"},
+    {file = "numpy-1.25.1-cp311-cp311-win32.whl", hash = "sha256:791f409064d0a69dd20579345d852c59822c6aa087f23b07b1b4e28ff5880fcb"},
+    {file = "numpy-1.25.1-cp311-cp311-win_amd64.whl", hash = "sha256:c40571fe966393b212689aa17e32ed905924120737194b5d5c1b20b9ed0fb171"},
+    {file = "numpy-1.25.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3d7abcdd85aea3e6cdddb59af2350c7ab1ed764397f8eec97a038ad244d2d105"},
+    {file = "numpy-1.25.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1a180429394f81c7933634ae49b37b472d343cccb5bb0c4a575ac8bbc433722f"},
+    {file = "numpy-1.25.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d412c1697c3853c6fc3cb9751b4915859c7afe6a277c2bf00acf287d56c4e625"},
+    {file = "numpy-1.25.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20e1266411120a4f16fad8efa8e0454d21d00b8c7cee5b5ccad7565d95eb42dd"},
+    {file = "numpy-1.25.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f76aebc3358ade9eacf9bc2bb8ae589863a4f911611694103af05346637df1b7"},
+    {file = "numpy-1.25.1-cp39-cp39-win32.whl", hash = "sha256:247d3ffdd7775bdf191f848be8d49100495114c82c2bd134e8d5d075fb386a1c"},
+    {file = "numpy-1.25.1-cp39-cp39-win_amd64.whl", hash = "sha256:1d5d3c68e443c90b38fdf8ef40e60e2538a27548b39b12b73132456847f4b631"},
+    {file = "numpy-1.25.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:35a9527c977b924042170a0887de727cd84ff179e478481404c5dc66b4170009"},
+    {file = "numpy-1.25.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d3fe3dd0506a28493d82dc3cf254be8cd0d26f4008a417385cbf1ae95b54004"},
+    {file = "numpy-1.25.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:012097b5b0d00a11070e8f2e261128c44157a8689f7dedcf35576e525893f4fe"},
+    {file = "numpy-1.25.1.tar.gz", hash = "sha256:9a3a9f3a61480cc086117b426a8bd86869c213fc4072e606f01c4e4b66eb92bf"},
 ]
 
 [[package]]
 name = "packaging"
-version = "21.3"
+version = "23.1"
 description = "Core utilities for Python packages"
+category = "dev"
 optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
 files = [
-    {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
-    {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
+    {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"},
+    {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"},
 ]
 
-[package.dependencies]
-pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
+[[package]]
+name = "pathspec"
+version = "0.11.1"
+description = "Utility library for gitignore style pattern matching of file paths."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"},
+    {file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"},
+]
 
 [[package]]
 name = "pefile"
-version = "2022.5.30"
+version = "2023.2.7"
 description = "Python PE parsing module"
+category = "dev"
 optional = false
 python-versions = ">=3.6.0"
 files = [
-    {file = "pefile-2022.5.30.tar.gz", hash = "sha256:a5488a3dd1fd021ce33f969780b88fe0f7eebb76eb20996d7318f307612a045b"},
+    {file = "pefile-2023.2.7-py3-none-any.whl", hash = "sha256:da185cd2af68c08a6cd4481f7325ed600a88f6a813bad9dea07ab3ef73d8d8d6"},
+    {file = "pefile-2023.2.7.tar.gz", hash = "sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc"},
 ]
 
-[package.dependencies]
-future = "*"
-
 [[package]]
 name = "pillow"
 version = "9.3.0"
 description = "Python Imaging Library (Fork)"
+category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -285,25 +419,62 @@ files = [
 docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinxext-opengraph"]
 tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"]
 
+[[package]]
+name = "platformdirs"
+version = "3.9.1"
+description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "platformdirs-3.9.1-py3-none-any.whl", hash = "sha256:ad8291ae0ae5072f66c16945166cb11c63394c7a3ad1b1bc9828ca3162da8c2f"},
+    {file = "platformdirs-3.9.1.tar.gz", hash = "sha256:1b42b450ad933e981d56e59f1b97495428c9bd60698baab9f3eb3d00d5822421"},
+]
+
+[package.extras]
+docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"]
+test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)"]
+
 [[package]]
 name = "pluggy"
-version = "1.0.0"
+version = "1.2.0"
 description = "plugin and hook calling mechanisms for python"
+category = "dev"
 optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
 files = [
-    {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
-    {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
+    {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"},
+    {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"},
 ]
 
 [package.extras]
 dev = ["pre-commit", "tox"]
 testing = ["pytest", "pytest-benchmark"]
 
+[[package]]
+name = "pre-commit"
+version = "3.3.3"
+description = "A framework for managing and maintaining multi-language pre-commit hooks."
+category = "dev"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "pre_commit-3.3.3-py2.py3-none-any.whl", hash = "sha256:10badb65d6a38caff29703362271d7dca483d01da88f9d7e05d0b97171c136cb"},
+    {file = "pre_commit-3.3.3.tar.gz", hash = "sha256:a2256f489cd913d575c145132ae196fe335da32d91a8294b7afe6622335dd023"},
+]
+
+[package.dependencies]
+cfgv = ">=2.0.0"
+identify = ">=1.0.0"
+nodeenv = ">=0.11.1"
+pyyaml = ">=5.1"
+virtualenv = ">=20.10.0"
+
 [[package]]
 name = "pycodestyle"
 version = "2.10.0"
 description = "Python style guide checker"
+category = "dev"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -315,6 +486,7 @@ files = [
 name = "pyflakes"
 version = "3.0.1"
 description = "passive checker of Python programs"
+category = "dev"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -324,22 +496,24 @@ files = [
 
 [[package]]
 name = "pyinstaller"
-version = "5.6.2"
+version = "5.13.0"
 description = "PyInstaller bundles a Python application and all its dependencies into a single package."
+category = "dev"
 optional = false
-python-versions = "<3.12,>=3.7"
+python-versions = "<3.13,>=3.7"
 files = [
-    {file = "pyinstaller-5.6.2-py3-none-macosx_10_13_universal2.whl", hash = "sha256:1b1e3b37a22fb36555d917f0c3dfb998159ff4af6d8fa7cc0074d630c6fe81ad"},
-    {file = "pyinstaller-5.6.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:05df5d2b9ca645cc6ef61d8a85451d2aabe5501997f1f50cd94306fd6bc0485d"},
-    {file = "pyinstaller-5.6.2-py3-none-manylinux2014_i686.whl", hash = "sha256:eb083c25f711769af0898852ea30dcb727ba43990bbdf9ffbaa9c77a7bd0d720"},
-    {file = "pyinstaller-5.6.2-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:0d167d57036219914188f1400427dd297b975707e78c32a5511191e607be920a"},
-    {file = "pyinstaller-5.6.2-py3-none-manylinux2014_s390x.whl", hash = "sha256:32727232f446aa96e394f01b0c35b3de0dc3513c6ba3e26d1ef64c57edb1e9e5"},
-    {file = "pyinstaller-5.6.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:181856ade585b090379ae26b7017dc2c30620e36e3a804b381417a6dc3b2a82b"},
-    {file = "pyinstaller-5.6.2-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:77888f52b61089caa0bee70809bbce9e9b1c613c88b6cb0742ff2a45f1511cbb"},
-    {file = "pyinstaller-5.6.2-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:d888db9afedff290d362ee296d30eb339abeba707ca1565916ce1cd5947131c3"},
-    {file = "pyinstaller-5.6.2-py3-none-win32.whl", hash = "sha256:e026adc92c60158741d0bfca27eefaa2414801f61328cb84d0c88241fe8c2087"},
-    {file = "pyinstaller-5.6.2-py3-none-win_amd64.whl", hash = "sha256:04ecf805bde2ef25b8e3642410871e6747c22fa7254107f155b8cd179c2a13b6"},
-    {file = "pyinstaller-5.6.2.tar.gz", hash = "sha256:865025b6809d777bb0f66d8f8ab50cc97dc3dbe0ff09a1ef1f2fd646432714fc"},
+    {file = "pyinstaller-5.13.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:7fdd319828de679f9c5e381eff998ee9b4164bf4457e7fca56946701cf002c3f"},
+    {file = "pyinstaller-5.13.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0df43697c4914285ecd333be968d2cd042ab9b2670124879ee87931d2344eaf5"},
+    {file = "pyinstaller-5.13.0-py3-none-manylinux2014_i686.whl", hash = "sha256:28d9742c37e9fb518444b12f8c8ab3cb4ba212d752693c34475c08009aa21ccf"},
+    {file = "pyinstaller-5.13.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e5fb17de6c325d3b2b4ceaeb55130ad7100a79096490e4c5b890224406fa42f4"},
+    {file = "pyinstaller-5.13.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:78975043edeb628e23a73fb3ef0a273cda50e765f1716f75212ea3e91b09dede"},
+    {file = "pyinstaller-5.13.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:cd7d5c06f2847195a23d72ede17c60857d6f495d6f0727dc6c9bc1235f2eb79c"},
+    {file = "pyinstaller-5.13.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:24009eba63cfdbcde6d2634e9c87f545eb67249ddf3b514e0cd3b2cdaa595828"},
+    {file = "pyinstaller-5.13.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:1fde4381155f21d6354dc450dcaa338cd8a40aaacf6bd22b987b0f3e1f96f3ee"},
+    {file = "pyinstaller-5.13.0-py3-none-win32.whl", hash = "sha256:2d03419904d1c25c8968b0ad21da0e0f33d8d65716e29481b5bd83f7f342b0c5"},
+    {file = "pyinstaller-5.13.0-py3-none-win_amd64.whl", hash = "sha256:9fc27c5a853b14a90d39c252707673c7a0efec921cd817169aff3af0fca8c127"},
+    {file = "pyinstaller-5.13.0-py3-none-win_arm64.whl", hash = "sha256:3a331951f9744bc2379ea5d65d36f3c828eaefe2785f15039592cdc08560b262"},
+    {file = "pyinstaller-5.13.0.tar.gz", hash = "sha256:5e446df41255e815017d96318e39f65a3eb807e74a796c7e7ff7f13b6366a2e9"},
 ]
 
 [package.dependencies]
@@ -347,8 +521,8 @@ altgraph = "*"
 macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""}
 pefile = {version = ">=2022.5.30", markers = "sys_platform == \"win32\""}
 pyinstaller-hooks-contrib = ">=2021.4"
-pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""}
-setuptools = "*"
+pywin32-ctypes = {version = ">=0.2.1", markers = "sys_platform == \"win32\""}
+setuptools = ">=42.0.0"
 
 [package.extras]
 encryption = ["tinyaes (>=1.0.0)"]
@@ -356,97 +530,87 @@ hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"]
 
 [[package]]
 name = "pyinstaller-hooks-contrib"
-version = "2022.13"
+version = "2023.6"
 description = "Community maintained hooks for PyInstaller"
+category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "pyinstaller-hooks-contrib-2022.13.tar.gz", hash = "sha256:e06d0881e599d94dc39c6ed1917f0ad9b1858a2478b9892faac18bd48bcdc2de"},
-    {file = "pyinstaller_hooks_contrib-2022.13-py2.py3-none-any.whl", hash = "sha256:91ecb30db757a8db8b6661d91d5df99e0998245f05f5cfaade0550922c7030a3"},
+    {file = "pyinstaller-hooks-contrib-2023.6.tar.gz", hash = "sha256:596a72009d8692b043e0acbf5e1b476d93149900142ba01845dded91a0770cb5"},
+    {file = "pyinstaller_hooks_contrib-2023.6-py2.py3-none-any.whl", hash = "sha256:aa6d7d038814df6aa7bec7bdbebc7cb4c693d3398df858f6062957f0797d397b"},
 ]
 
-[[package]]
-name = "pyparsing"
-version = "3.0.9"
-description = "pyparsing module - Classes and methods to define and execute parsing grammars"
-optional = false
-python-versions = ">=3.6.8"
-files = [
-    {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
-    {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
-]
-
-[package.extras]
-diagrams = ["jinja2", "railroad-diagrams"]
-
 [[package]]
 name = "pyqt6"
-version = "6.4.0"
+version = "6.5.2"
 description = "Python bindings for the Qt cross platform application toolkit"
+category = "main"
 optional = false
 python-versions = ">=3.6.1"
 files = [
-    {file = "PyQt6-6.4.0-cp37-abi3-macosx_10_14_universal2.whl", hash = "sha256:8e614cc4b95dec8bec8ba3c3eaf821058b45862e66d69a8c4fb5576123f2be98"},
-    {file = "PyQt6-6.4.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:79d093ce97010eac92e83a4e8efe231328af9ec757ba78c77e46ca1e007eca0d"},
-    {file = "PyQt6-6.4.0-cp37-abi3-win_amd64.whl", hash = "sha256:37e399420866fff5c5bd49f56710dffd3a7a126afc6612b877d666f381bd554f"},
-    {file = "PyQt6-6.4.0.tar.gz", hash = "sha256:91392469be1f491905fa9e78fa4e4059a89ab616ddf2ecfd525bc1d65c26bb93"},
+    {file = "PyQt6-6.5.2-cp37-abi3-macosx_10_14_universal2.whl", hash = "sha256:5bad1437eb0c1ae801103b32ef04ef62ef1cce505b448525f60089ce36329b89"},
+    {file = "PyQt6-6.5.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:70468cca4537756c714a57fa1baa5beabb9b38775f52f9611f49870705672c55"},
+    {file = "PyQt6-6.5.2-cp37-abi3-win_amd64.whl", hash = "sha256:ff1d12767b578f0f0e87cdb12198e7dcad9a176c40d1d1d799984181b0af93cb"},
+    {file = "PyQt6-6.5.2.tar.gz", hash = "sha256:1487ee7350f9ffb66d60ab4176519252c2b371762cbe8f8340fd951f63801280"},
 ]
 
 [package.dependencies]
-PyQt6-Qt6 = ">=6.4.0"
+PyQt6-Qt6 = ">=6.5.0"
 PyQt6-sip = ">=13.4,<14"
 
 [[package]]
 name = "pyqt6-qt6"
-version = "6.4.1"
+version = "6.5.2"
 description = "The subset of a Qt installation needed by PyQt6."
+category = "main"
 optional = false
 python-versions = "*"
 files = [
-    {file = "PyQt6_Qt6-6.4.1-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:b47087932f10cc39dab63cd0d2d23d7e3e204547c5e6559adfe58d665fa8ee4f"},
-    {file = "PyQt6_Qt6-6.4.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:0de4aebdc469c61a36d43b868f4b45d89bb94aaf13779ed5c0080da4b416997c"},
-    {file = "PyQt6_Qt6-6.4.1-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:791e9f0115bdc9ff22bf807b573393f2b9f7e2d1f07fc2d8351350ca141ebaf1"},
-    {file = "PyQt6_Qt6-6.4.1-py3-none-win_amd64.whl", hash = "sha256:31a4557fed53e96feedae46624e420bafd261c0c01d92a1a1aad2c1646144f56"},
+    {file = "PyQt6_Qt6-6.5.2-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:4b37f6f93c0980469ccc570998d3e3de243028bae7389fb6330443ab215ce2f6"},
+    {file = "PyQt6_Qt6-6.5.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8dad61b4666d91882d7e1c4d619c71e7429c13e19182f8b3bebf3ecf95107d4c"},
+    {file = "PyQt6_Qt6-6.5.2-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:953f3c0e99e486081a6d438b32fbc240da97457226562eb68cf1b11c516386fd"},
+    {file = "PyQt6_Qt6-6.5.2-py3-none-win_amd64.whl", hash = "sha256:5a3c7bb899678bf801136b31cd589ed4d0d54ab32be5fb76c2bdeb161a9662ad"},
 ]
 
 [[package]]
 name = "pyqt6-sip"
-version = "13.4.0"
+version = "13.5.2"
 description = "The sip module support for PyQt6"
+category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "PyQt6_sip-13.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3ac7e0800180202dcc0c7035ff88c2a6f4a0f5acb20c4a19f71d807d0f7857b7"},
-    {file = "PyQt6_sip-13.4.0-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:bb4f2e2fdcf3a8dafe4256750bbedd9e7107c4fd8afa9c25be28423c36bb12b8"},
-    {file = "PyQt6_sip-13.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:de601187055d684b36ebe6e800a5deacaa55b69d71ad43312b76422cfeae0e12"},
-    {file = "PyQt6_sip-13.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e3b17308ca729bcb6d25c01144c6b2e17d40812231c3ef9caaa72a78db2b1069"},
-    {file = "PyQt6_sip-13.4.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d51704d50b82713fd7c928b7deb31e17be239ddac74fc2fd708e52bd21ecea3a"},
-    {file = "PyQt6_sip-13.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:77af9c7e3f50414ec5af9b1534aaf2ba25115ae65aa5ed735111c8ef0884b862"},
-    {file = "PyQt6_sip-13.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:83b446d247a92d119d507dbc94fc1f47389d8118a5b6232a2859951157319a30"},
-    {file = "PyQt6_sip-13.4.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:802b0cfed19900183220c46895c2635f0dd062f2d275a25506423f911ef74db4"},
-    {file = "PyQt6_sip-13.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2694ae67811cefb6ea3ee0e9995755b45e4952f4dcadec8c04300fd828f91c75"},
-    {file = "PyQt6_sip-13.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9e03c780846fd5d71c06e33322fd11a05713d9cd410e3571d069ac9a84077647"},
-    {file = "PyQt6_sip-13.4.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:9c5231536e6153071b22175e46e368045fd08d772a90d772a0977d1166c7822c"},
-    {file = "PyQt6_sip-13.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:cfdcd43e123bd55c155cfe7a33895033e6d25de90a48ed9c25a0ddd0196a810e"},
-    {file = "PyQt6_sip-13.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3de18c4a32f717a351d560a39f528af24077f5135aacfa8890a2f2d79f0633da"},
-    {file = "PyQt6_sip-13.4.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:fd2df2a9479b8ac858117bd55dc0ec0bd1ce6c8f3486b0cc5e860c0540a8bba0"},
-    {file = "PyQt6_sip-13.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:3486914137f5336cff6e10a5e9d52c1e60ff883473938b45f267f794daeacb2f"},
-    {file = "PyQt6_sip-13.4.0.tar.gz", hash = "sha256:6d87a3ee5872d7511b76957d68a32109352caf3b7a42a01d9ee20032b350d979"},
+    {file = "PyQt6_sip-13.5.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c0b4e55c4d0dc44728da90eb1451dfff0d05260b4a3496ff0014494e6c1865a6"},
+    {file = "PyQt6_sip-13.5.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9ad5b8c4c92d62e00ebf254a4c9656668b130f2a1d2792034e0d82b2d6571509"},
+    {file = "PyQt6_sip-13.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:5d0fd42da765198b51d7fe12c29721cbe3e14b77ca4f68aa43618149ee7dbeff"},
+    {file = "PyQt6_sip-13.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4b5b0c4b557e0293eba008021342459a0f91c69a7a2eb5231276d495d1d2960a"},
+    {file = "PyQt6_sip-13.5.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:831f5d606fc5296a80303ab30892c3308954c5766039bf7a96267488bb2524a5"},
+    {file = "PyQt6_sip-13.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:91812f0094443b816a74a89954d60bb50060807f54d7c016a4de7bd29454091e"},
+    {file = "PyQt6_sip-13.5.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:94afb031db89159aa330891eba2c937b0378b4b264570998848c7a78eddf7c94"},
+    {file = "PyQt6_sip-13.5.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ace942b78378bff8ae2d6bafccc84465f1ff0cf30720b8321e0bd6c95c36ede6"},
+    {file = "PyQt6_sip-13.5.2-cp37-cp37m-win_amd64.whl", hash = "sha256:318d4d1c7ef60f69c68227cef78fc439accc9028ec6149200eea3d34c433d373"},
+    {file = "PyQt6_sip-13.5.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:87d758ba999baa16459f0a3c7f7ed47a5b45e8991ad691f17345bf3c493a4281"},
+    {file = "PyQt6_sip-13.5.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:5b499eff7150d9f31fe835a73cc41f076bba2fcde0f5b0325b1284797f17c0ac"},
+    {file = "PyQt6_sip-13.5.2-cp38-cp38-win_amd64.whl", hash = "sha256:1bdb1f7b5b2f6ac31d517a6f3a13c38055640ac0014f67a2e5422d2083ce69ec"},
+    {file = "PyQt6_sip-13.5.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9c9dac067710015895f523f5a2a4d59cbef8624a152b6f9a426e5b848d8c6d29"},
+    {file = "PyQt6_sip-13.5.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:dcf602c233ee7600e810927adcb9e518d61bc796a6b2013c17feedd24c4e5413"},
+    {file = "PyQt6_sip-13.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:b54b0d8d21c5835af8f6d6d8a323a23106e67b7cd4c31e23c35bb4c321000de8"},
+    {file = "PyQt6_sip-13.5.2.tar.gz", hash = "sha256:ebf6264b6feda01ba37d3b60a4bb87493bdb87be70f7b2a5384a7acd4902d88d"},
 ]
 
 [[package]]
 name = "pytest"
-version = "7.2.0"
+version = "7.4.0"
 description = "pytest: simple powerful testing with Python"
+category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"},
-    {file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"},
+    {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"},
+    {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"},
 ]
 
 [package.dependencies]
-attrs = ">=19.2.0"
 colorama = {version = "*", markers = "sys_platform == \"win32\""}
 exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
 iniconfig = "*"
@@ -455,77 +619,156 @@ pluggy = ">=0.12,<2.0"
 tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
 
 [package.extras]
-testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
+testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
 
 [[package]]
 name = "pywin32-ctypes"
-version = "0.2.0"
-description = ""
+version = "0.2.2"
+description = "A (partial) reimplementation of pywin32 using ctypes/cffi"
+category = "dev"
 optional = false
-python-versions = "*"
+python-versions = ">=3.6"
 files = [
-    {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"},
-    {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"},
+    {file = "pywin32-ctypes-0.2.2.tar.gz", hash = "sha256:3426e063bdd5fd4df74a14fa3cf80a0b42845a87e1d1e81f6549f9daec593a60"},
+    {file = "pywin32_ctypes-0.2.2-py3-none-any.whl", hash = "sha256:bf490a1a709baf35d688fe0ecf980ed4de11d2b3e37b51e5442587a75d9957e7"},
+]
+
+[[package]]
+name = "pyyaml"
+version = "6.0.1"
+description = "YAML parser and emitter for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+files = [
+    {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"},
+    {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"},
+    {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
+    {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
+    {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
+    {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
+    {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
+    {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
+    {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"},
+    {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
+    {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
+    {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
+    {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
+    {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"},
+    {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"},
+    {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
+    {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
+    {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
+    {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
+    {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
+    {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
+    {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"},
+    {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
+    {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
+    {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
+    {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
+    {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
+    {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
+]
+
+[[package]]
+name = "ruff"
+version = "0.0.280"
+description = "An extremely fast Python linter, written in Rust."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "ruff-0.0.280-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:48ed5aca381050a4e2f6d232db912d2e4e98e61648b513c350990c351125aaec"},
+    {file = "ruff-0.0.280-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:ef6ee3e429fd29d6a5ceed295809e376e6ece5b0f13c7e703efaf3d3bcb30b96"},
+    {file = "ruff-0.0.280-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d878370f7e9463ac40c253724229314ff6ebe4508cdb96cb536e1af4d5a9cd4f"},
+    {file = "ruff-0.0.280-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:83e8f372fa5627eeda5b83b5a9632d2f9c88fc6d78cead7e2a1f6fb05728d137"},
+    {file = "ruff-0.0.280-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7008fc6ca1df18b21fa98bdcfc711dad5f94d0fc3c11791f65e460c48ef27c82"},
+    {file = "ruff-0.0.280-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:fe7118c1eae3fda17ceb409629c7f3b5a22dffa7caf1f6796776936dca1fe653"},
+    {file = "ruff-0.0.280-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37359cd67d2af8e09110a546507c302cbea11c66a52d2a9b6d841d465f9962d4"},
+    {file = "ruff-0.0.280-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd58af46b0221efb95966f1f0f7576df711cb53e50d2fdb0e83c2f33360116a4"},
+    {file = "ruff-0.0.280-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e7c15828d09f90e97bea8feefcd2907e8c8ce3a1f959c99f9b4b3469679f33c"},
+    {file = "ruff-0.0.280-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2dae8f2d9c44c5c49af01733c2f7956f808db682a4193180dedb29dd718d7bbe"},
+    {file = "ruff-0.0.280-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5f972567163a20fb8c2d6afc60c2ea5ef8b68d69505760a8bd0377de8984b4f6"},
+    {file = "ruff-0.0.280-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8ffa7347ad11643f29de100977c055e47c988cd6d9f5f5ff83027600b11b9189"},
+    {file = "ruff-0.0.280-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7a37dab70114671d273f203268f6c3366c035fe0c8056614069e90a65e614bfc"},
+    {file = "ruff-0.0.280-py3-none-win32.whl", hash = "sha256:7784e3606352fcfb193f3cd22b2e2117c444cb879ef6609ec69deabd662b0763"},
+    {file = "ruff-0.0.280-py3-none-win_amd64.whl", hash = "sha256:4a7d52457b5dfcd3ab24b0b38eefaead8e2dca62b4fbf10de4cd0938cf20ce30"},
+    {file = "ruff-0.0.280-py3-none-win_arm64.whl", hash = "sha256:b7de5b8689575918e130e4384ed9f539ce91d067c0a332aedef6ca7188adac2d"},
+    {file = "ruff-0.0.280.tar.gz", hash = "sha256:581c43e4ac5e5a7117ad7da2120d960a4a99e68ec4021ec3cd47fe1cf78f8380"},
 ]
 
 [[package]]
 name = "scipy"
-version = "1.10.0"
+version = "1.11.1"
 description = "Fundamental algorithms for scientific computing in Python"
-optional = false
-python-versions = "<3.12,>=3.8"
-files = [
-    {file = "scipy-1.10.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:b901b423c91281a974f6cd1c36f5c6c523e665b5a6d5e80fcb2334e14670eefd"},
-    {file = "scipy-1.10.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:16ba05d3d1b9f2141004f3f36888e05894a525960b07f4c2bfc0456b955a00be"},
-    {file = "scipy-1.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:151f066fe7d6653c3ffefd489497b8fa66d7316e3e0d0c0f7ff6acca1b802809"},
-    {file = "scipy-1.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f9ea0a37aca111a407cb98aa4e8dfde6e5d9333bae06dfa5d938d14c80bb5c3"},
-    {file = "scipy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:27e548276b5a88b51212b61f6dda49a24acf5d770dff940bd372b3f7ced8c6c2"},
-    {file = "scipy-1.10.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:42ab8b9e7dc1ebe248e55f54eea5307b6ab15011a7883367af48dd781d1312e4"},
-    {file = "scipy-1.10.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:e096b062d2efdea57f972d232358cb068413dc54eec4f24158bcbb5cb8bddfd8"},
-    {file = "scipy-1.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4df25a28bd22c990b22129d3c637fd5c3be4b7c94f975dca909d8bab3309b694"},
-    {file = "scipy-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ad449db4e0820e4b42baccefc98ec772ad7818dcbc9e28b85aa05a536b0f1a2"},
-    {file = "scipy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:6faf86ef7717891195ae0537e48da7524d30bc3b828b30c9b115d04ea42f076f"},
-    {file = "scipy-1.10.0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:4bd0e3278126bc882d10414436e58fa3f1eca0aa88b534fcbf80ed47e854f46c"},
-    {file = "scipy-1.10.0-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:38bfbd18dcc69eeb589811e77fae552fa923067fdfbb2e171c9eac749885f210"},
-    {file = "scipy-1.10.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ab2a58064836632e2cec31ca197d3695c86b066bc4818052b3f5381bfd2a728"},
-    {file = "scipy-1.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5cd7a30970c29d9768a7164f564d1fbf2842bfc77b7d114a99bc32703ce0bf48"},
-    {file = "scipy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:9b878c671655864af59c108c20e4da1e796154bd78c0ed6bb02bc41c84625686"},
-    {file = "scipy-1.10.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:3afcbddb4488ac950ce1147e7580178b333a29cd43524c689b2e3543a080a2c8"},
-    {file = "scipy-1.10.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:6e4497e5142f325a5423ff5fda2fff5b5d953da028637ff7c704378c8c284ea7"},
-    {file = "scipy-1.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:441cab2166607c82e6d7a8683779cb89ba0f475b983c7e4ab88f3668e268c143"},
-    {file = "scipy-1.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0490dc499fe23e4be35b8b6dd1e60a4a34f0c4adb30ac671e6332446b3cbbb5a"},
-    {file = "scipy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:954ff69d2d1bf666b794c1d7216e0a746c9d9289096a64ab3355a17c7c59db54"},
-    {file = "scipy-1.10.0.tar.gz", hash = "sha256:c8b3cbc636a87a89b770c6afc999baa6bcbb01691b5ccbbc1b1791c7c0a07540"},
+category = "main"
+optional = false
+python-versions = "<3.13,>=3.9"
+files = [
+    {file = "scipy-1.11.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:aec8c62fbe52914f9cf28d846cf0401dd80ab80788bbab909434eb336ed07c04"},
+    {file = "scipy-1.11.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:3b9963798df1d8a52db41a6fc0e6fa65b1c60e85d73da27ae8bb754de4792481"},
+    {file = "scipy-1.11.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e8eb42db36526b130dfbc417609498a6192381abc1975b91e3eb238e0b41c1a"},
+    {file = "scipy-1.11.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:366a6a937110d80dca4f63b3f5b00cc89d36f678b2d124a01067b154e692bab1"},
+    {file = "scipy-1.11.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:08d957ca82d3535b3b9ba6c8ff355d78fe975271874e2af267cb5add5bd78625"},
+    {file = "scipy-1.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:e866514bc2d660608447b6ba95c8900d591f2865c07cca0aa4f7ff3c4ca70f30"},
+    {file = "scipy-1.11.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ba94eeef3c9caa4cea7b402a35bb02a5714ee1ee77eb98aca1eed4543beb0f4c"},
+    {file = "scipy-1.11.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:512fdc18c65f76dadaca139348e525646d440220d8d05f6d21965b8d4466bccd"},
+    {file = "scipy-1.11.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cce154372f0ebe88556ed06d7b196e9c2e0c13080ecb58d0f35062dc7cc28b47"},
+    {file = "scipy-1.11.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4bb943010203465ac81efa392e4645265077b4d9e99b66cf3ed33ae12254173"},
+    {file = "scipy-1.11.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:249cfa465c379c9bb2c20123001e151ff5e29b351cbb7f9c91587260602c58d0"},
+    {file = "scipy-1.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:ffb28e3fa31b9c376d0fb1f74c1f13911c8c154a760312fbee87a21eb21efe31"},
+    {file = "scipy-1.11.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:39154437654260a52871dfde852adf1b93b1d1bc5dc0ffa70068f16ec0be2624"},
+    {file = "scipy-1.11.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:b588311875c58d1acd4ef17c983b9f1ab5391755a47c3d70b6bd503a45bfaf71"},
+    {file = "scipy-1.11.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d51565560565a0307ed06fa0ec4c6f21ff094947d4844d6068ed04400c72d0c3"},
+    {file = "scipy-1.11.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b41a0f322b4eb51b078cb3441e950ad661ede490c3aca66edef66f4b37ab1877"},
+    {file = "scipy-1.11.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:396fae3f8c12ad14c5f3eb40499fd06a6fef8393a6baa352a652ecd51e74e029"},
+    {file = "scipy-1.11.1-cp39-cp39-win_amd64.whl", hash = "sha256:be8c962a821957fdde8c4044efdab7a140c13294997a407eaee777acf63cbf0c"},
+    {file = "scipy-1.11.1.tar.gz", hash = "sha256:fb5b492fa035334fd249f0973cc79ecad8b09c604b42a127a677b45a9a3d4289"},
 ]
 
 [package.dependencies]
-numpy = ">=1.19.5,<1.27.0"
+numpy = ">=1.21.6,<1.28.0"
 
 [package.extras]
-dev = ["click", "doit (>=0.36.0)", "flake8", "mypy", "pycodestyle", "pydevtool", "rich-click", "typing_extensions"]
-doc = ["matplotlib (>2)", "numpydoc", "pydata-sphinx-theme (==0.9.0)", "sphinx (!=4.1.0)", "sphinx-design (>=0.2.0)"]
+dev = ["click", "cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy", "pycodestyle", "pydevtool", "rich-click", "ruff", "types-psutil", "typing_extensions"]
+doc = ["jupytext", "matplotlib (>2)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (==0.9.0)", "sphinx (!=4.1.0)", "sphinx-design (>=0.2.0)"]
 test = ["asv", "gmpy2", "mpmath", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"]
 
 [[package]]
 name = "setuptools"
-version = "65.6.3"
+version = "68.0.0"
 description = "Easily download, build, install, upgrade, and uninstall Python packages"
+category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "setuptools-65.6.3-py3-none-any.whl", hash = "sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54"},
-    {file = "setuptools-65.6.3.tar.gz", hash = "sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75"},
+    {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"},
+    {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"},
 ]
 
 [package.extras]
-docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
-testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
+docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
+testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
 testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
 
 [[package]]
 name = "six"
 version = "1.16.0"
 description = "Python 2 and 3 compatibility utilities"
+category = "main"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
 files = [
@@ -535,13 +778,14 @@ files = [
 
 [[package]]
 name = "termcolor"
-version = "2.1.1"
+version = "2.3.0"
 description = "ANSI color formatting for output in terminal"
+category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "termcolor-2.1.1-py3-none-any.whl", hash = "sha256:fa852e957f97252205e105dd55bbc23b419a70fec0085708fc0515e399f304fd"},
-    {file = "termcolor-2.1.1.tar.gz", hash = "sha256:67cee2009adc6449c650f6bcf3bdeed00c8ba53a8cda5362733c53e0a39fb70b"},
+    {file = "termcolor-2.3.0-py3-none-any.whl", hash = "sha256:3afb05607b89aed0ffe25202399ee0867ad4d3cb4180d98aaf8eefa6a5f7d475"},
+    {file = "termcolor-2.3.0.tar.gz", hash = "sha256:b5b08f68937f138fe92f6c089b99f1e2da0ae56c52b78bf7075fd95420fd9a5a"},
 ]
 
 [package.extras]
@@ -551,6 +795,7 @@ tests = ["pytest", "pytest-cov"]
 name = "tomli"
 version = "2.0.1"
 description = "A lil' TOML parser"
+category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -560,13 +805,14 @@ files = [
 
 [[package]]
 name = "tqdm"
-version = "4.64.1"
+version = "4.65.0"
 description = "Fast, Extensible Progress Meter"
+category = "main"
 optional = false
-python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
+python-versions = ">=3.7"
 files = [
-    {file = "tqdm-4.64.1-py2.py3-none-any.whl", hash = "sha256:6fee160d6ffcd1b1c68c65f14c829c22832bc401726335ce92c52d395944a6a1"},
-    {file = "tqdm-4.64.1.tar.gz", hash = "sha256:5f4f682a004951c1b450bc753c710e9280c5746ce6ffedee253ddbcbf54cf1e4"},
+    {file = "tqdm-4.65.0-py3-none-any.whl", hash = "sha256:c4f53a17fe37e132815abceec022631be8ffe1b9381c2e6e30aa70edc99e9671"},
+    {file = "tqdm-4.65.0.tar.gz", hash = "sha256:1871fb68a86b8fb3b59ca4cdd3dcccbc7e6d613eeed31f4c332531977b89beb5"},
 ]
 
 [package.dependencies]
@@ -578,10 +824,44 @@ notebook = ["ipywidgets (>=6)"]
 slack = ["slack-sdk"]
 telegram = ["requests"]
 
+[[package]]
+name = "typing-extensions"
+version = "4.7.1"
+description = "Backported and Experimental Type Hints for Python 3.7+"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"},
+    {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"},
+]
+
+[[package]]
+name = "virtualenv"
+version = "20.24.2"
+description = "Virtual Python Environment builder"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "virtualenv-20.24.2-py3-none-any.whl", hash = "sha256:43a3052be36080548bdee0b42919c88072037d50d56c28bd3f853cbe92b953ff"},
+    {file = "virtualenv-20.24.2.tar.gz", hash = "sha256:fd8a78f46f6b99a67b7ec5cf73f92357891a7b3a40fd97637c27f854aae3b9e0"},
+]
+
+[package.dependencies]
+distlib = ">=0.3.7,<1"
+filelock = ">=3.12.2,<4"
+platformdirs = ">=3.9.1,<4"
+
+[package.extras]
+docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
+test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"]
+
 [[package]]
 name = "win32-setctime"
 version = "1.1.0"
 description = "A small Python utility to set file creation time on Windows"
+category = "main"
 optional = false
 python-versions = ">=3.5"
 files = [
@@ -594,5 +874,5 @@ dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"]
 
 [metadata]
 lock-version = "2.0"
-python-versions = ">=3.8.1,<3.11"
-content-hash = "327c08fb31b9cd6746778b81e5bb1ec7df7357cf6da74274d7f37b1af9c1c5b6"
+python-versions = ">=3.9,<3.12"
+content-hash = "8a9af4c5c0897796abcca762602f99d56c417804e5f43d8ea5cdc9f04ece36c4"
diff --git a/pyproject.toml b/pyproject.toml
index 2842f79..fb778d2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -14,19 +14,22 @@ packages = [
 glint-mask = 'glint_mask_generator.cli:main'
 
 [tool.poetry.dependencies]
-python = ">=3.8.1,<3.11"
-numpy = "~=1.23.5"
-fire = "~=0.4.0"
+python = ">=3.9,<3.12"
+numpy = "~=1.25.1"
+fire = "~=0.5.0"
 Pillow = "~=9.3.0"
-scipy = ">=1.9.3,<1.11.0"
-tqdm = "~=4.64.1"
-loguru = "~=0.6.0"
+scipy = "~=1.11.1"
+tqdm = "~=4.65.0"
+loguru = "~=0.7.0"
 pyqt6 = "^6.4.0"
 
 [tool.poetry.group.dev.dependencies]
 pytest = "^7.2.0"
 flake8 = "^6.0.0"
 pyinstaller = "^5.6.2"
+pre-commit = "^3.3.3"
+ruff = "^0.0.280"
+black = "^23.7.0"
 
 [build-system]
 requires = ["poetry-core"]


Test patch:
diff --git a/.github/workflows/test_code.yml b/.github/workflows/test_code.yml
index c1787c6..6a4b3f5 100644
--- a/.github/workflows/test_code.yml
+++ b/.github/workflows/test_code.yml
@@ -18,7 +18,7 @@ jobs:
     strategy:
       matrix:
         os: [Ubuntu, Windows]
-        python-version: ['3.8', '3.9', '3.10']
+        python-version: ['3.9', '3.10', '3.11']
     steps:
       - uses: actions/checkout@v3
       - name: Set up Python ${{ matrix.python-version }}
@@ -45,4 +45,4 @@ jobs:
 
       - name: Test with pytest
         run: |
-          poetry run pytest -v
\ No newline at end of file
+          poetry run pytest -v
diff --git a/tests/test_image_loaders.py b/tests/test_image_loaders.py
index 81d9390..3fe1fb7 100644
--- a/tests/test_image_loaders.py
+++ b/tests/test_image_loaders.py
@@ -12,17 +12,44 @@
 from glint_mask_generator.image_loaders import P4MSLoader, MicasenseRedEdgeLoader
 
 IMG_CONTENT = Image.fromarray(np.ones((32, 32, 3)).astype(np.uint8) * 255)
+TEST_FILE_NAMES = [
+    "1.jpg",
+    "2.JPG",
+    "3.tif",
+    "4.tiff",
+    "5.TIF",
+    "6.TIFF",
+    "7.png",
+    "8.PNG",
+    "9.jpeg",
+    "10.JPEG",
+    "IMG_1234_1.tif",
+    "IMG_1234_2.tif",
+    "IMG_1234_3.tif",
+    "IMG_1234_4.tif",
+    "IMG_1234_5.tif",
+    "IMG_4321_1.TIF",
+    "IMG_4321_2.TIF",
+    "IMG_4321_3.TIF",
+    "IMG_4321_4.TIF",
+    "IMG_4321_5.TIF",
+    "DJI_1021.TIF",
+    "DJI_1022.TIF",
+    "DJI_1023.TIF",
+    "DJI_1024.TIF",
+    "DJI_1025.TIF",
+    "DJI_2021.tif",
+    "DJI_2022.tif",
+    "DJI_2023.tif",
+    "DJI_2024.tif",
+    "DJI_2025.tif",
+]
 
 
 def test_p4ms_loader(tmp_path):
     """Test that DJI Multispectral masker finds the correct files."""
     # Create dummy files
-    all_paths = ["1.jpg", "2.JPG", "3.tif", "4.tiff", "5.TIF", "6.TIFF", "7.png", "8.PNG", "9.jpeg", "10.JPEG",
-                 "IMG_1234_1.tif", "IMG_1234_2.tif", "IMG_1234_3.tif", "IMG_1234_4.tif", "IMG_1234_5.tif",
-                 "IMG_4321_1.TIF", "IMG_4321_2.TIF", "IMG_4321_3.TIF", "IMG_4321_4.TIF", "IMG_4321_5.TIF",
-                 "DJI_1021.TIF", "DJI_1022.TIF", "DJI_1023.TIF", "DJI_1024.TIF", "DJI_1025.TIF",
-                 "DJI_2021.tif", "DJI_2022.tif", "DJI_2023.tif", "DJI_2024.tif", "DJI_2025.tif"]
-    all_paths = sorted([str(tmp_path.joinpath(n)) for n in all_paths])
+    all_paths = sorted([str(tmp_path.joinpath(n)) for n in TEST_FILE_NAMES])
     for name in all_paths:
         IMG_CONTENT.save(name)
 
@@ -30,8 +57,20 @@ def test_p4ms_loader(tmp_path):
     image_loader = P4MSLoader(tmp_path, str(Path(tmp_path).joinpath("masks")))
 
     valid_paths = [
-        ["DJI_1021.TIF", "DJI_1022.TIF", "DJI_1023.TIF", "DJI_1024.TIF", "DJI_1025.TIF"],
-        ["DJI_2021.TIF", "DJI_2022.TIF", "DJI_2023.TIF", "DJI_2024.TIF", "DJI_2025.TIF"]
+        [
+            "DJI_1021.TIF",
+            "DJI_1022.TIF",
+            "DJI_1023.TIF",
+            "DJI_1024.TIF",
+            "DJI_1025.TIF",
+        ],
+        [
+            "DJI_2021.TIF",
+            "DJI_2022.TIF",
+            "DJI_2023.TIF",
+            "DJI_2024.TIF",
+            "DJI_2025.TIF",
+        ],
     ]
     valid_paths = [[os.path.join(tmp_path, p) for p in paths] for paths in valid_paths]
     valid_paths = sorted(valid_paths, key=lambda a: a[0])
@@ -68,21 +107,23 @@ def test_p4ms_loader(tmp_path):
     assert image_loader._is_blue_band_path("/home/wherever/dji_0011.tif") is True
     assert image_loader._is_blue_band_path("/home/wherever/DJI_0013.TIF") is False
 
-    assert image_loader._is_blue_band_path(Path("C:\\Users\\some\\dir\\dji_0011.tif")) is True
-    assert not image_loader._is_blue_band_path(Path("C:\\Users\\some\\dir\\DJI_0013.TIF"))
+    assert (
+        image_loader._is_blue_band_path(Path("C:\\Users\\some\\dir\\dji_0011.tif"))
+        is True
+    )
+    assert not image_loader._is_blue_band_path(
+        Path("C:\\Users\\some\\dir\\DJI_0013.TIF")
+    )
     assert image_loader._is_blue_band_path("C:\\Users\\some\\dir\\dji_0011.tif") is True
-    assert image_loader._is_blue_band_path("C:\\Users\\some\\dir\\DJI_0013.TIF") is False
+    assert (
+        image_loader._is_blue_band_path("C:\\Users\\some\\dir\\DJI_0013.TIF") is False
+    )
 
 
 def test_micasense_red_edge_masker(tmp_path):
     """Test that the Micasense masker finds the correct files."""
     # Create dummy files
-    all_paths = ["1.jpg", "2.JPG", "3.tif", "4.tiff", "5.TIF", "6.TIFF", "7.png", "8.PNG", "9.jpeg", "10.JPEG",
-                 "IMG_1234_1.tif", "IMG_1234_2.tif", "IMG_1234_3.tif", "IMG_1234_4.tif", "IMG_1234_5.tif",
-                 "IMG_4321_1.TIF", "IMG_4321_2.TIF", "IMG_4321_3.TIF", "IMG_4321_4.TIF", "IMG_4321_5.TIF",
-                 "DJI_1021.TIF", "DJI_1022.TIF", "DJI_1023.TIF", "DJI_1024.TIF", "DJI_1025.TIF",
-                 "DJI_2021.tif", "DJI_2022.tif", "DJI_2023.tif", "DJI_2024.tif", "DJI_2025.tif"]
-    all_paths = sorted([str(tmp_path.joinpath(n)) for n in all_paths])
+    all_paths = sorted([str(tmp_path.joinpath(n)) for n in TEST_FILE_NAMES])
     for name in all_paths:
         IMG_CONTENT.save(name)
 
@@ -90,8 +131,20 @@ def test_micasense_red_edge_masker(tmp_path):
     image_loader = MicasenseRedEdgeLoader(tmp_path, Path(tmp_path).joinpath("masks"))
 
     valid_paths = [
-        ["IMG_1234_1.tif", "IMG_1234_2.tif", "IMG_1234_3.tif", "IMG_1234_4.tif", "IMG_1234_5.tif"],
-        ["IMG_4321_1.tif", "IMG_4321_2.tif", "IMG_4321_3.tif", "IMG_4321_4.tif", "IMG_4321_5.tif"]
+        [
+            "IMG_1234_1.tif",
+            "IMG_1234_2.tif",
+            "IMG_1234_3.tif",
+            "IMG_1234_4.tif",
+            "IMG_1234_5.tif",
+        ],
+        [
+            "IMG_4321_1.tif",
+            "IMG_4321_2.tif",
+            "IMG_4321_3.tif",
+            "IMG_4321_4.tif",
+            "IMG_4321_5.tif",
+        ],
     ]
     valid_paths = [[os.path.join(tmp_path, p) for p in paths] for paths in valid_paths]
     valid_paths = sorted(valid_paths, key=lambda a: a[0])
@@ -135,7 +188,17 @@ def test_micasense_red_edge_masker(tmp_path):
     assert image_loader._is_blue_band_path("/home/dir/img_3332_1.tif") is True
     assert image_loader._is_blue_band_path("/home/dir/img_3332_5.tif") is False
 
-    assert image_loader._is_blue_band_path(Path("C:\\Users\\some\\dir\\img_3332_1.tif")) is True
-    assert image_loader._is_blue_band_path(Path("C:\\Users\\some\\dir\\img_3332_5.tif")) is False
-    assert image_loader._is_blue_band_path("C:\\Users\\some\\dir\\img_3332_1.tif") is True
-    assert image_loader._is_blue_band_path("C:\\Users\\some\\dir\\img_3332_5.tif") is False
+    assert (
+        image_loader._is_blue_band_path(Path("C:\\Users\\some\\dir\\img_3332_1.tif"))
+        is True
+    )
+    assert (
+        image_loader._is_blue_band_path(Path("C:\\Users\\some\\dir\\img_3332_5.tif"))
+        is False
+    )
+    assert (
+        image_loader._is_blue_band_path("C:\\Users\\some\\dir\\img_3332_1.tif") is True
+    )
+    assert (
+        image_loader._is_blue_band_path("C:\\Users\\some\\dir\\img_3332_5.tif") is False
+    )