diff --git a/.github/workflows/python_package.yml b/.github/workflows/test_python_package.yml similarity index 63% rename from .github/workflows/python_package.yml rename to .github/workflows/test_python_package.yml index d052c21f..3045b69d 100644 --- a/.github/workflows/python_package.yml +++ b/.github/workflows/test_python_package.yml @@ -1,8 +1,8 @@ # Installs the Python dependencies, installs Crappy, and checks that it imports -name: Python Package +name: Test Python Package on: - # Runs on pull requests targeting the default branch + # Runs on pull requests targeting the default branches pull_request: types: [opened, edited, reopened, synchronize] branches: ["master", "develop"] @@ -15,24 +15,35 @@ on: - cron: '0 12 1 * *' jobs: - build: + test-python-package: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: + # Run on all the supported Python versions python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] + # Run on all the supported platforms os: [ubuntu-latest, windows-latest, macos-latest] + # Cannot run for Python 3.7 on macOS as it is no longer supported + exclude: + - os: macos-latest + python-version: 3.7 steps: + # Checkout the repository - name: Checkout uses: actions/checkout@v4 + # Set up the correct version of Python - name: Set up Python ${{ matrix.python-version }} on ${{ matrix.os }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + # Install the build dependencies - name: Install dependencies run: python -m pip install --upgrade pip wheel build setuptools + # Install the crappy Python module - name: Install Crappy run: python -m pip install . + # Check if the module imports as expected - name: Import Crappy run: python -c "import crappy; print(crappy.__version__)" diff --git a/docs/source/conf.py b/docs/source/conf.py index 5b3d4d65..b4655a93 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -16,7 +16,7 @@ from time import gmtime, strftime from re import match -__version__ = '2.0.5' +__version__ = '2.0.6' # -- Project information ----------------------------------------------------- diff --git a/examples/blocks/generator/generator_custom_condition.py b/examples/blocks/generator/generator_custom_condition.py new file mode 100644 index 00000000..37176de5 --- /dev/null +++ b/examples/blocks/generator/generator_custom_condition.py @@ -0,0 +1,100 @@ +# coding: utf-8 + +""" +This example demonstrates the use of a Generator Block using a user-defined +condition to determine whether to switch to the next Path. It does not require +any specific hardware to run, but necessitates the matplotlib Python module to +be installed. + +The Generator Block outputs a signal following a provided path. Several paths +are available, each with a different behavior and different options. They can +be combined to form a custom global path. + +Here, the Generator outputs a simple constant signal, that switches to a +different value once the end condition is met. However, unlike the other +Generator example, the stop condition is not one of the standard ones defined +in Crappy, but rather an arbitrary callable defined by the user. Here, the +condition check whether a given file exists, but it could really have been any +other kind of condition. + +After starting this script, you should create the file 'test.txt' in the same +folder where this script is located. See how the value of the signal changes +once the file is created. Once you delete the newly created file, the test +should then end, due to the second custom condition. You can also end this demo +earlier by clicking on the stop button that appears. You can also hit CTRL+C, +but it is not a clean way to stop Crappy. +""" + +import crappy +from pathlib import Path + + +def file_exists(data): + """Returns True if the file 'test.txt' exists at the same level as the + running script, False otherwise. + + This arbitrary function can access the data received by the Generator Block, + which is exposed in the data argument as a dictionary. + + Args: + data: The data received by the Generator Block since its last loop. The + keys are the labels, and the values a list containing all the received + values for the given label. + """ + + return Path('./test.txt').exists() + + +def file_does_not_exist(data): + """Returns False if the file 'test.txt' exists at the same level as the + running script, True otherwise. + + This arbitrary function can access the data received by the Generator Block, + which is exposed in the data argument as a dictionary. + + Args: + data: The data received by the Generator Block since its last loop. The + keys are the labels, and the values a list containing all the received + values for the given label. + """ + + return not Path('./test.txt').exists() + + +if __name__ == '__main__': + + # This Generator Block generates a constant signal, and sends it to the + # Dashboard Block for display + # The signal first has a value of 0, then 1. + gen = crappy.blocks.Generator( + path=({'type': 'Constant', 'value': 0, + 'condition': file_exists}, + {'type': 'Constant', 'value': 1, + 'condition': file_does_not_exist}), + # The simple path to generate + # Notice how the functions defined earlier are included in the path and + # associated to the 'condition' key + freq=50, # Lowering the default frequency because it's just a demo + cmd_label='signal', # The label carrying the value of the generated + # signal + path_index_label='index', # This label carries the index of the current + # path + spam=True, # Send a value at each loop, for a nice display on the + # Dashboard + + # Sticking to default for the other arguments + ) + + # This Dashboard displays the signal it receives from the Generator + dash = crappy.blocks.Dashboard(('t(s)', 'signal')) + + # This Block allows the user to properly exit the script + stop = crappy.blocks.StopButton( + # No specific argument to give for this Block + ) + + # Linking the Block so that the information is correctly sent and received + crappy.link(gen, dash) + + # Mandatory line for starting the test, this call is blocking + crappy.start() diff --git a/pyproject.toml b/pyproject.toml index 596d3faf..dd327e0e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "crappy" dynamic = ["readme"] -version = "2.0.5" +version = "2.0.6" description = "Command and Real-time Acquisition in Parallelized Python" license = {file = "LICENSE"} keywords = ["control", "command", "acquisition", "multiprocessing"] diff --git a/src/crappy/__version__.py b/src/crappy/__version__.py index 7717659c..2a6cb509 100644 --- a/src/crappy/__version__.py +++ b/src/crappy/__version__.py @@ -1,3 +1,3 @@ # coding: utf-8 -__version__ = '2.0.5' +__version__ = '2.0.6' diff --git a/src/crappy/blocks/camera.py b/src/crappy/blocks/camera.py index 55598bac..912b4c7a 100644 --- a/src/crappy/blocks/camera.py +++ b/src/crappy/blocks/camera.py @@ -194,20 +194,21 @@ def __init__(self, Ignored if ``save_images`` is :obj:`False`. .. versionadded:: 1.5.10 - save_backend: If ``save_images`` is :obj:`True`, the backend to use for + save_backend: If ``save_images`` is :obj:`True`, the backend to use for recording the images. It should be one of: :: - 'sitk', 'cv2', 'pil', 'npy' + 'sitk', 'pil', 'cv2', 'npy' - They correspond to the modules :mod:`SimpleITK`, :mod:`cv2` (OpenCV), - :mod:`PIL` (Pillow Fork), and :mod:`numpy`. Note that the ``'npy'`` + They correspond to the modules :mod:`SimpleITK`, :mod:`PIL` (Pillow + Fork), :mod:`cv2` (OpenCV), and :mod:`numpy`. Note that the ``'npy'`` backend saves the images as raw :obj:`numpy.array`, and thus ignores the ``img_extension`` argument. Depending on the machine, some backends may be faster or slower. For using each backend, the corresponding - Python must of course be installed. If not provided and ``save_images`` - is :obj:`True`, the backends are tried in the same order as given above - and the first available one is used. ``'npy'`` is always available. + Python module must of course be installed. If not provided and + ``save_images`` is :obj:`True`, the backends are tried in the same + order as given above and the first available one is used. ``'npy'`` is + always available. .. versionadded:: 1.5.10 image_generator: A callable taking two :obj:`float` as arguments and diff --git a/src/crappy/blocks/camera_processes/display.py b/src/crappy/blocks/camera_processes/display.py index 8f276693..2d9c503f 100644 --- a/src/crappy/blocks/camera_processes/display.py +++ b/src/crappy/blocks/camera_processes/display.py @@ -172,8 +172,8 @@ def loop(self) -> None: if self.img.dtype != np.uint8: self.log(logging.DEBUG, f"Casting displayed image from " f"{self.img.dtype} to uint8") - if np.max(self.img) > 255: - factor = max(ceil(log2(np.max(self.img) + 1) - 8), 0) + if int(np.max(self.img)) > 255: + factor = max(ceil(log2(int(np.max(self.img)) + 1) - 8), 0) img = (self.img / 2 ** factor).astype(np.uint8) else: img = self.img.astype(np.uint8) diff --git a/src/crappy/blocks/camera_processes/gpu_correl.py b/src/crappy/blocks/camera_processes/gpu_correl.py index 8b70e1e0..49f839f3 100644 --- a/src/crappy/blocks/camera_processes/gpu_correl.py +++ b/src/crappy/blocks/camera_processes/gpu_correl.py @@ -213,7 +213,8 @@ def loop(self) -> None: self._res_history.append(res) self._res_history = self._res_history[-self._discard_ref - 1:] - if res > self._discard_limit * np.average(self._res_history[:-1]): + if (res > self._discard_limit * + float(np.average(self._res_history[:-1]))): self.log(logging.WARNING, "Residual too high, not sending " "values") return diff --git a/src/crappy/blocks/camera_processes/record.py b/src/crappy/blocks/camera_processes/record.py index 601d5aef..db65271c 100644 --- a/src/crappy/blocks/camera_processes/record.py +++ b/src/crappy/blocks/camera_processes/record.py @@ -226,16 +226,25 @@ def loop(self) -> None: # Saving the image at the destination path using the chosen backend self.log(logging.DEBUG, "Saving image") if self._save_backend == 'sitk': - Sitk.WriteImage(Sitk.GetImageFromArray(self.img), path) + if len(self.img.shape) == 3: + Sitk.WriteImage(Sitk.GetImageFromArray(self.img[:, :, ::-1], + isVector=True), path) + else: + Sitk.WriteImage(Sitk.GetImageFromArray(self.img), path) + + elif self._save_backend == 'pil': + if len(self.img.shape) == 3: + PIL.Image.fromarray(self.img[:, :, ::-1]).save( + path, exif={TAGS_INV[key]: val for key, val in self.metadata.items() + if key in TAGS_INV}) + else: + PIL.Image.fromarray(self.img).save( + path, exif={TAGS_INV[key]: val for key, val in self.metadata.items() + if key in TAGS_INV}) elif self._save_backend == 'cv2': cv2.imwrite(path, self.img) - elif self._save_backend == 'pil': - PIL.Image.fromarray(self.img).save( - path, exif={TAGS_INV[key]: val for key, val in self.metadata.items() - if key in TAGS_INV}) - elif self._save_backend == 'npy': np.save(path, self.img) diff --git a/src/crappy/blocks/dic_ve.py b/src/crappy/blocks/dic_ve.py index 7985c9cc..1512a9e6 100644 --- a/src/crappy/blocks/dic_ve.py +++ b/src/crappy/blocks/dic_ve.py @@ -208,16 +208,17 @@ def __init__(self, recording the images. It should be one of: :: - 'sitk', 'cv2', 'pil', 'npy' + 'sitk', 'pil', 'cv2', 'npy' - They correspond to the modules :mod:`SimpleITK`, :mod:`cv2` (OpenCV), - :mod:`PIL` (Pillow Fork), and :mod:`numpy`. Note that the ``'npy'`` + They correspond to the modules :mod:`SimpleITK`, :mod:`PIL` (Pillow + Fork), :mod:`cv2` (OpenCV), and :mod:`numpy`. Note that the ``'npy'`` backend saves the images as raw :obj:`numpy.array`, and thus ignores the ``img_extension`` argument. Depending on the machine, some backends may be faster or slower. For using each backend, the corresponding - Python must of course be installed. If not provided and ``save_images`` - is :obj:`True`, the backends are tried in the same order as given above - and the first available one is used. ``'npy'`` is always available. + Python module must of course be installed. If not provided and + ``save_images`` is :obj:`True`, the backends are tried in the same + order as given above and the first available one is used. ``'npy'`` is + always available. .. versionadded:: 1.5.10 image_generator: A callable taking two :obj:`float` as arguments and diff --git a/src/crappy/blocks/dis_correl.py b/src/crappy/blocks/dis_correl.py index b22993b7..c95ba3c0 100644 --- a/src/crappy/blocks/dis_correl.py +++ b/src/crappy/blocks/dis_correl.py @@ -198,16 +198,17 @@ def __init__(self, recording the images. It should be one of: :: - 'sitk', 'cv2', 'pil', 'npy' + 'sitk', 'pil', 'cv2', 'npy' - They correspond to the modules :mod:`SimpleITK`, :mod:`cv2` (OpenCV), - :mod:`PIL` (Pillow Fork), and :mod:`numpy`. Note that the ``'npy'`` + They correspond to the modules :mod:`SimpleITK`, :mod:`PIL` (Pillow + Fork), :mod:`cv2` (OpenCV), and :mod:`numpy`. Note that the ``'npy'`` backend saves the images as raw :obj:`numpy.array`, and thus ignores the ``img_extension`` argument. Depending on the machine, some backends may be faster or slower. For using each backend, the corresponding - Python must of course be installed. If not provided and ``save_images`` - is :obj:`True`, the backends are tried in the same order as given above - and the first available one is used. ``'npy'`` is always available. + Python module must of course be installed. If not provided and + ``save_images`` is :obj:`True`, the backends are tried in the same + order as given above and the first available one is used. ``'npy'`` is + always available. .. versionadded:: 1.5.10 image_generator: A callable taking two :obj:`float` as arguments and diff --git a/src/crappy/blocks/fake_machine.py b/src/crappy/blocks/fake_machine.py index 88663ada..82ced1bb 100644 --- a/src/crappy/blocks/fake_machine.py +++ b/src/crappy/blocks/fake_machine.py @@ -138,10 +138,11 @@ def loop(self) -> None: # Calculating the speed based on the command and the mode if self._mode == 'speed': - speed = np.sign(cmd) * np.min((self._max_speed, np.abs(cmd))) + speed = float(np.sign(cmd)) * float(np.min((self._max_speed, + np.abs(cmd)))) elif self._mode == 'position': - speed = np.sign(cmd - self._current_pos) * np.min( - (self._max_speed, np.abs(cmd - self._current_pos) / delta_t)) + speed = float(np.sign(cmd - self._current_pos)) * float(np.min( + (self._max_speed, np.abs(cmd - self._current_pos) / delta_t))) else: raise ValueError(f'Invalid mode : {self._mode} !') diff --git a/src/crappy/blocks/gpu_correl.py b/src/crappy/blocks/gpu_correl.py index 20a656c7..fa538657 100644 --- a/src/crappy/blocks/gpu_correl.py +++ b/src/crappy/blocks/gpu_correl.py @@ -200,16 +200,17 @@ def __init__(self, recording the images. It should be one of: :: - 'sitk', 'cv2', 'pil', 'npy' + 'sitk', 'pil', 'cv2', 'npy' - They correspond to the modules :mod:`SimpleITK`, :mod:`cv2` (OpenCV), - :mod:`PIL` (Pillow Fork), and :mod:`numpy`. Note that the ``'npy'`` + They correspond to the modules :mod:`SimpleITK`, :mod:`PIL` (Pillow + Fork), :mod:`cv2` (OpenCV), and :mod:`numpy`. Note that the ``'npy'`` backend saves the images as raw :obj:`numpy.array`, and thus ignores the ``img_extension`` argument. Depending on the machine, some backends may be faster or slower. For using each backend, the corresponding - Python must of course be installed. If not provided and ``save_images`` - is :obj:`True`, the backends are tried in the same order as given above - and the first available one is used. ``'npy'`` is always available. + Python module must of course be installed. If not provided and + ``save_images`` is :obj:`True`, the backends are tried in the same + order as given above and the first available one is used. ``'npy'`` is + always available. .. versionadded:: 1.5.10 image_generator: A callable taking two :obj:`float` as arguments and diff --git a/src/crappy/blocks/gpu_ve.py b/src/crappy/blocks/gpu_ve.py index d9975e9f..74c85124 100644 --- a/src/crappy/blocks/gpu_ve.py +++ b/src/crappy/blocks/gpu_ve.py @@ -186,16 +186,17 @@ def __init__(self, recording the images. It should be one of: :: - 'sitk', 'cv2', 'pil', 'npy' + 'sitk', 'pil', 'cv2', 'npy' - They correspond to the modules :mod:`SimpleITK`, :mod:`cv2` (OpenCV), - :mod:`PIL` (Pillow Fork), and :mod:`numpy`. Note that the ``'npy'`` + They correspond to the modules :mod:`SimpleITK`, :mod:`PIL` (Pillow + Fork), :mod:`cv2` (OpenCV), and :mod:`numpy`. Note that the ``'npy'`` backend saves the images as raw :obj:`numpy.array`, and thus ignores the ``img_extension`` argument. Depending on the machine, some backends may be faster or slower. For using each backend, the corresponding - Python must of course be installed. If not provided and ``save_images`` - is :obj:`True`, the backends are tried in the same order as given above - and the first available one is used. ``'npy'`` is always available. + Python module must of course be installed. If not provided and + ``save_images`` is :obj:`True`, the backends are tried in the same + order as given above and the first available one is used. ``'npy'`` is + always available. .. versionadded:: 1.5.10 image_generator: A callable taking two :obj:`float` as arguments and diff --git a/src/crappy/blocks/mean.py b/src/crappy/blocks/mean.py index 96a77749..c61b5854 100644 --- a/src/crappy/blocks/mean.py +++ b/src/crappy/blocks/mean.py @@ -112,7 +112,7 @@ def loop(self) -> None: for label, values in data.items(): if self._out_labels is None or label in self._out_labels: try: - to_send[label] = np.mean(values) + to_send[label] = float(np.mean(values)) except (ValueError, TypeError): self.log(logging.WARNING, f"Cannot perform averaging on label " f"{label} with values: {values}") diff --git a/src/crappy/blocks/video_extenso.py b/src/crappy/blocks/video_extenso.py index 2241a850..f5e80612 100644 --- a/src/crappy/blocks/video_extenso.py +++ b/src/crappy/blocks/video_extenso.py @@ -193,16 +193,17 @@ def __init__(self, recording the images. It should be one of: :: - 'sitk', 'cv2', 'pil', 'npy' + 'sitk', 'pil', 'cv2', 'npy' - They correspond to the modules :mod:`SimpleITK`, :mod:`cv2` (OpenCV), - :mod:`PIL` (Pillow Fork), and :mod:`numpy`. Note that the ``'npy'`` + They correspond to the modules :mod:`SimpleITK`, :mod:`PIL` (Pillow + Fork), :mod:`cv2` (OpenCV), and :mod:`numpy`. Note that the ``'npy'`` backend saves the images as raw :obj:`numpy.array`, and thus ignores the ``img_extension`` argument. Depending on the machine, some backends may be faster or slower. For using each backend, the corresponding - Python must of course be installed. If not provided and ``save_images`` - is :obj:`True`, the backends are tried in the same order as given above - and the first available one is used. ``'npy'`` is always available. + Python module must of course be installed. If not provided and + ``save_images`` is :obj:`True`, the backends are tried in the same + order as given above and the first available one is used. ``'npy'`` is + always available. .. versionadded:: 1.5.10 image_generator: A callable taking two :obj:`float` as arguments and diff --git a/src/crappy/camera/seek_thermal_pro.py b/src/crappy/camera/seek_thermal_pro.py index ae0da8fa..048571fc 100644 --- a/src/crappy/camera/seek_thermal_pro.py +++ b/src/crappy/camera/seek_thermal_pro.py @@ -265,7 +265,8 @@ def _correct_dead_pixels(self, img: np.ndarray) -> np.ndarray: """ for i, j in self._dead_pixels: - img[i, j] = np.median(img[max(0, i - 1): i + 2, max(0, j - 1): j + 2]) + img[i, j] = float(np.median(img[max(0, i - 1): i + 2, + max(0, j - 1): j + 2])) return img def _write_data(self, request: int, data: bytes) -> int: diff --git a/src/crappy/lamcube/bispectral.py b/src/crappy/lamcube/bispectral.py index 472a6002..170ab444 100644 --- a/src/crappy/lamcube/bispectral.py +++ b/src/crappy/lamcube/bispectral.py @@ -3,6 +3,7 @@ import numpy as np from typing import Tuple import logging +from warnings import warn from ..camera.cameralink import BaslerIronmanCameraLink @@ -86,11 +87,16 @@ class BiSpectral(BaslerIronmanCameraLink): .. versionadded:: 1.4.0 .. versionchanged:: 2.0.0 renamed from *Bispectral* to *BiSpectral* + .. versionremoved:: 2.1.0 """ def __init__(self) -> None: """Adds the various setting for the Camera.""" + warn(f"Starting from version 2.1.0, {type(self).__name__} will be " + f"deprecated and removed from Crappy. Please contact the maintainers " + f"if you still use this Camera.", FutureWarning) + super().__init__() self.add_scale_setting('width', 1, 640, self._get_w, self._set_w, 640) self.add_scale_setting('height', 1, 512, self._get_h, self._set_h, 512) diff --git a/src/crappy/modifier/demux.py b/src/crappy/modifier/demux.py index 4a88112a..b1772bbb 100644 --- a/src/crappy/modifier/demux.py +++ b/src/crappy/modifier/demux.py @@ -86,24 +86,24 @@ def __call__(self, data: Dict[str, np.ndarray]) -> Dict[str, Any]: # The data of a given label is on a same row if self._transpose: if self._mean: - data[label] = np.mean(data[self._stream_label][i, :]) + data[label] = float(np.mean(data[self._stream_label][i, :])) else: - data[label] = data[self._stream_label][i, 0] + data[label] = float(data[self._stream_label][i, 0]) # The data of a given label is on a same column else: if self._mean: - data[label] = np.mean(data[self._stream_label][:, i]) + data[label] = float(np.mean(data[self._stream_label][:, i])) else: - data[label] = data[self._stream_label][0, i] + data[label] = float(data[self._stream_label][0, i]) # Discarding the raw data del data[self._stream_label] # Keeping either the average or the first time value if self._mean: - data[self._time_label] = np.mean(data[self._time_label]) + data[self._time_label] = float(np.mean(data[self._time_label])) else: - data[self._time_label] = np.squeeze(data[self._time_label])[0] + data[self._time_label] = float(np.squeeze(data[self._time_label])[0]) self.log(logging.DEBUG, f"Sending {data}") diff --git a/src/crappy/modifier/mean.py b/src/crappy/modifier/mean.py index 7ce733a5..83633545 100644 --- a/src/crappy/modifier/mean.py +++ b/src/crappy/modifier/mean.py @@ -54,7 +54,7 @@ def __call__(self, data: Dict[str, Any]) -> Optional[Dict[str, Any]]: # Once there's enough data in the buffer, calculating the average value if len(self._buf[label]) == self._n_points: try: - ret[label] = np.mean(self._buf[label]) + ret[label] = float(np.mean(self._buf[label])) except TypeError: ret[label] = self._buf[label][-1] diff --git a/src/crappy/modifier/median.py b/src/crappy/modifier/median.py index 998e53b5..36c8e222 100644 --- a/src/crappy/modifier/median.py +++ b/src/crappy/modifier/median.py @@ -54,7 +54,7 @@ def __call__(self, data: Dict[str, Any]) -> Optional[Dict[str, Any]]: # Once there's enough data in the buffer, calculating the median value if len(self._buf[label]) == self._n_points: try: - ret[label] = np.median(self._buf[label]) + ret[label] = float(np.median(self._buf[label])) except TypeError: ret[label] = self._buf[label][-1] diff --git a/src/crappy/modifier/moving_avg.py b/src/crappy/modifier/moving_avg.py index 4c7f4381..398a236f 100644 --- a/src/crappy/modifier/moving_avg.py +++ b/src/crappy/modifier/moving_avg.py @@ -55,7 +55,7 @@ def __call__(self, data: Dict[str, Any]) -> Dict[str, Any]: # Calculating the average for each label try: - ret[label] = np.mean(self._buf[label]) + ret[label] = float(np.mean(self._buf[label])) except TypeError: ret[label] = self._buf[label][-1] diff --git a/src/crappy/modifier/moving_med.py b/src/crappy/modifier/moving_med.py index 80f0dd6c..c75f99e5 100644 --- a/src/crappy/modifier/moving_med.py +++ b/src/crappy/modifier/moving_med.py @@ -55,7 +55,7 @@ def __call__(self, data: Dict[str, Any]) -> Dict[str, Any]: # Calculating the median for each label try: - ret[label] = np.median(self._buf[label]) + ret[label] = float(np.median(self._buf[label])) except TypeError: ret[label] = self._buf[label][-1] diff --git a/src/crappy/tool/camera_config/camera_config.py b/src/crappy/tool/camera_config/camera_config.py index 9662bbee..e0801d71 100644 --- a/src/crappy/tool/camera_config/camera_config.py +++ b/src/crappy/tool/camera_config/camera_config.py @@ -112,6 +112,7 @@ def __init__(self, self._run = True self._n_loops = 0 self._max_freq = max_freq + self._got_first_img: bool = False # Settings for adjusting the behavior of the zoom self._zoom_ratio = 0.9 @@ -521,13 +522,13 @@ def _update_pixel_value(self) -> None: self.log(logging.DEBUG, "Updating the value of the current pixel") try: - self._reticle_val.set(np.average(self._original_img[self._y_pos.get(), - self._x_pos.get()])) + self._reticle_val.set(int(np.average( + self._original_img[self._y_pos.get(), self._x_pos.get()]))) except IndexError: self._x_pos.set(0) self._y_pos.set(0) - self._reticle_val.set(np.average(self._original_img[self._y_pos.get(), - self._x_pos.get()])) + self._reticle_val.set(int(np.average( + self._original_img[self._y_pos.get(), self._x_pos.get()]))) def _coord_to_pix(self, x: int, y: int) -> Tuple[int, int]: """Converts the coordinates of the mouse in the GUI referential to @@ -858,19 +859,20 @@ def _cast_img(self, img: np.ndarray) -> None: # If the auto_range is set, adjusting the values to the range if self._auto_range.get(): self.log(logging.DEBUG, "Applying auto range to the image") - self._low_thresh, self._high_thresh = np.percentile(img, (3, 97)) + self._low_thresh, self._high_thresh = map(float, + np.percentile(img, (3, 97))) self._img = ((np.clip(img, self._low_thresh, self._high_thresh) - self._low_thresh) * 255 / (self._high_thresh - self._low_thresh)).astype('uint8') # The original image still needs to be saved as 8-bits - bit_depth = np.ceil(np.log2(np.max(img) + 1)) + bit_depth = int(np.ceil(np.log2(int(np.max(img)) + 1))) self._original_img = (img / 2 ** (bit_depth - 8)).astype('uint8') # Or if the image is not already 8 bits, casting to 8 bits elif img.dtype != np.uint8: self.log(logging.DEBUG, "Casting the image to 8 bits") - bit_depth = np.ceil(np.log2(np.max(img) + 1)) + bit_depth = int(np.ceil(np.log2(int(np.max(img)) + 1))) self._img = (img / 2 ** (bit_depth - 8)).astype('uint8') self._original_img = np.copy(self._img) @@ -880,7 +882,7 @@ def _cast_img(self, img: np.ndarray) -> None: self._original_img = np.copy(img) # Updating the information - self._nb_bits.set(int(np.ceil(np.log2(np.max(img) + 1)))) + self._nb_bits.set(int(np.ceil(np.log2(int(np.max(img)) + 1)))) self._max_pixel.set(int(np.max(img))) self._min_pixel.set(int(np.min(img))) @@ -940,6 +942,8 @@ def _on_img_resize(self, _: Optional[tk.Event] = None) -> None: self.log(logging.DEBUG, "The image canvas was resized") + self._draw_overlay() + self._resize_img() self._display_img() self.update() @@ -1023,13 +1027,12 @@ def _update_img(self) -> None: ret = self._camera.get_image() # Flag raised if no image could be grabbed - no_img = False + no_img = ret is None # If no frame could be grabbed from the camera - if ret is None: - no_img = True + if no_img: # If it's the first call, generate error image to initialize the window - if not self._n_loops: + if not self._got_first_img: self.log(logging.WARNING, "Could not get an image from the camera, " "displaying an error image instead") ret = None, np.array(Image.open(BytesIO(resource_string( @@ -1041,6 +1044,8 @@ def _update_img(self) -> None: sleep(0.001) return + # Always set, so that the error image is only ever loaded once + self._got_first_img = True self._n_loops += 1 _, img = ret @@ -1050,6 +1055,7 @@ def _update_img(self) -> None: self.shape = img.shape self._cast_img(img) + self._draw_overlay() self._resize_img() self._calc_hist() @@ -1061,3 +1067,9 @@ def _update_img(self) -> None: self._update_pixel_value() self.update() + + def _draw_overlay(self) -> None: + """Method meant to be used by subclasses for drawing an overlay on top of + the image to display.""" + + ... diff --git a/src/crappy/tool/camera_config/camera_config_boxes.py b/src/crappy/tool/camera_config/camera_config_boxes.py index 069d47bf..4ec426b1 100644 --- a/src/crappy/tool/camera_config/camera_config_boxes.py +++ b/src/crappy/tool/camera_config/camera_config_boxes.py @@ -88,7 +88,7 @@ def _handle_box_outside_img(self, _: Box) -> None: """This method is meant to simplify the customization of the action to perform when a patch is outside the image in subclasses.""" - pass + ... def _draw_spots(self) -> None: """Simply draws every spot on top of the image.""" diff --git a/src/crappy/tool/camera_config/dic_ve_config.py b/src/crappy/tool/camera_config/dic_ve_config.py index 6b1230b6..5946a345 100644 --- a/src/crappy/tool/camera_config/dic_ve_config.py +++ b/src/crappy/tool/camera_config/dic_ve_config.py @@ -3,10 +3,6 @@ from typing import Optional from tkinter.messagebox import showerror import tkinter as tk -import numpy as np -from io import BytesIO -from pkg_resources import resource_string -from time import sleep import logging from multiprocessing.queues import Queue @@ -154,61 +150,10 @@ def _stop_box(self, _: tk.Event) -> None: # This box is not needed anymore self._select_box.reset() - def _on_img_resize(self, _: Optional[tk.Event] = None) -> None: - """Same as in the parent class except it also draws the patches on top of - the displayed image.""" + def _draw_overlay(self) -> None: + """Draws the detected spots to track on top of the last acquired image.""" - self.log(logging.DEBUG, "The image canvas was resized") - - self._draw_spots() - self._resize_img() - self._display_img() - self.update() - - def _update_img(self) -> None: - """Same as in the parent class except it also draws the patches on top of - the displayed image.""" - - self.log(logging.DEBUG, "Updating the image") - - ret = self._camera.get_image() - - # If no frame could be grabbed from the camera - if ret is None: - # If it's the first call, generate error image to initialize the window - if not self._n_loops: - self.log(logging.WARNING, "Could not get an image from the camera, " - "displaying an error image instead") - ret = None, np.array(Image.open(BytesIO(resource_string( - 'crappy', 'tool/data/no_image.png')))) - # Otherwise, just pass - else: - self.log(logging.DEBUG, "No image returned by the camera") - self.update() - sleep(0.001) - return - - self._n_loops += 1 - _, img = ret - - if img.dtype != self.dtype: - self.dtype = img.dtype - if self.shape != img.shape: - self.shape = img.shape - - self._cast_img(img) self._draw_spots() - self._resize_img() - - self._calc_hist() - self._resize_hist() - - self._display_img() - self._display_hist() - - self._update_pixel_value() - - self.update() def _handle_box_outside_img(self, box: Box) -> None: """If a patch is outside the image, warning the user and resetting the diff --git a/src/crappy/tool/camera_config/dis_correl_config.py b/src/crappy/tool/camera_config/dis_correl_config.py index da17bf95..2d5871d2 100644 --- a/src/crappy/tool/camera_config/dis_correl_config.py +++ b/src/crappy/tool/camera_config/dis_correl_config.py @@ -3,10 +3,6 @@ import tkinter as tk from tkinter.messagebox import showerror from typing import Optional -import numpy as np -from io import BytesIO -from pkg_resources import resource_string -from time import sleep import logging from multiprocessing.queues import Queue @@ -112,14 +108,7 @@ def _start_box(self, event: tk.Event) -> None: """Simply saves the position of the user click, and disables the display of the current correl box.""" - self.log(logging.DEBUG, "Starting the selection box") - - # If the mouse is on the canvas but not on the image, do nothing - if not self._check_event_pos(event): - return - - self._select_box.x_start, \ - self._select_box.y_start = self._coord_to_pix(event.x, event.y) + super()._start_box(event) self._draw_correl_box = False @@ -150,68 +139,16 @@ def _stop_box(self, _: tk.Event) -> None: self._draw_correl_box = True - def _on_img_resize(self, _: Optional[tk.Event] = None) -> None: - """Same as in the parent class except it also draws the select box on top - of the displayed image.""" + def _draw_overlay(self) -> None: + """Draws the box to use for performing correlation on top of the last + acquired image. - self.log(logging.DEBUG, "The image canvas was resized") - - # Do not draw the correl box if the user is creating the select box - if self._draw_correl_box: - self._draw_box(self._correl_box) - self._draw_box(self._select_box) + Does not draw the correl box is the user is using the selection box. + """ - self._resize_img() - self._display_img() - self.update() - - def _update_img(self) -> None: - """Same as in the parent class except it also draws the select box on top - of the displayed image.""" - - self.log(logging.DEBUG, "Updating the image") - - ret = self._camera.get_image() - - # If no frame could be grabbed from the camera - if ret is None: - # If it's the first call, generate error image to initialize the window - if not self._n_loops: - self.log(logging.WARNING, "Could not get an image from the camera, " - "displaying an error image instead") - ret = None, np.array(Image.open(BytesIO(resource_string( - 'crappy', 'tool/data/no_image.png')))) - # Otherwise, just pass - else: - self.log(logging.DEBUG, "No image returned by the camera") - self.update() - sleep(0.001) - return - - self._n_loops += 1 - _, img = ret - - if img.dtype != self.dtype: - self.dtype = img.dtype - if self.shape != img.shape: - self.shape = img.shape - - self._cast_img(img) - # Do not draw the correl box if the user is creating the select box if self._draw_correl_box: self._draw_box(self._correl_box) self._draw_box(self._select_box) - self._resize_img() - - self._calc_hist() - self._resize_hist() - - self._display_img() - self._display_hist() - - self._update_pixel_value() - - self.update() def _handle_box_outside_img(self, _: Box) -> None: """If the correl box is outside the image, it means that the image size has diff --git a/src/crappy/tool/camera_config/video_extenso_config.py b/src/crappy/tool/camera_config/video_extenso_config.py index cacbbe8a..b18d4198 100644 --- a/src/crappy/tool/camera_config/video_extenso_config.py +++ b/src/crappy/tool/camera_config/video_extenso_config.py @@ -3,10 +3,6 @@ import tkinter as tk from tkinter.messagebox import showerror from typing import Optional -import numpy as np -from io import BytesIO -from pkg_resources import resource_string -from time import sleep import logging from multiprocessing.queues import Queue @@ -115,10 +111,7 @@ def _create_buttons(self) -> None: """Compared with the parent class, creates an extra button for saving the original position of the spots.""" - self._update_button = tk.Button(self._sets_frame, text="Apply Settings", - command=self._update_settings) - self._update_button.pack(expand=False, fill='none', ipadx=5, ipady=5, - padx=5, pady=5, anchor='n', side='top') + super()._create_buttons() self._update_button = tk.Button(self._sets_frame, text="Save L0", command=self._save_l0) @@ -166,63 +159,14 @@ def _save_l0(self) -> None: f"Successfully saved L0 ! L0 x : {self._detector.spots.x_l0}, " f"L0 y : {self._detector.spots.y_l0}") - def _on_img_resize(self, _: Optional[tk.Event] = None) -> None: - """Same as in the parent class except it also draws the patches and the - select box on top of the displayed image.""" + def _draw_overlay(self) -> None: + """Draws the detected spots to track on top of the last acquired image. - self.log(logging.DEBUG, "The image canvas was resized") + Also draws the selection box if the user is currently drawing one. + """ self._draw_box(self._select_box) self._draw_spots() - self._resize_img() - self._display_img() - self.update() - - def _update_img(self) -> None: - """Same as in the parent class except it also draws the patches and the - select box on top of the displayed image.""" - - self.log(logging.DEBUG, "Updating the image") - - ret = self._camera.get_image() - - # If no frame could be grabbed from the camera - if ret is None: - # If it's the first call, generate error image to initialize the window - if not self._n_loops: - self.log(logging.WARNING, "Could not get an image from the camera, " - "displaying an error image instead") - ret = None, np.array(Image.open(BytesIO(resource_string( - 'crappy', 'tool/data/no_image.png')))) - # Otherwise, just pass - else: - self.log(logging.DEBUG, "No image returned by the camera") - self.update() - sleep(0.001) - return - - self._n_loops += 1 - _, img = ret - - if img.dtype != self.dtype: - self.dtype = img.dtype - if self.shape != img.shape: - self.shape = img.shape - - self._cast_img(img) - self._draw_box(self._select_box) - self._draw_spots() - self._resize_img() - - self._calc_hist() - self._resize_hist() - - self._display_img() - self._display_hist() - - self._update_pixel_value() - - self.update() def _handle_box_outside_img(self, _: Box) -> None: """If a patch is outside the image, it means that the image size has been diff --git a/src/crappy/tool/image_processing/dic_ve.py b/src/crappy/tool/image_processing/dic_ve.py index 38ba7f4c..b0b11fdc 100644 --- a/src/crappy/tool/image_processing/dic_ve.py +++ b/src/crappy/tool/image_processing/dic_ve.py @@ -307,7 +307,7 @@ def _parabola_fit(arr: np.ndarray) -> float: arr: This array contains the y values for the 3 points. """ - return (arr[0] - arr[2]) / (2 * (arr[0] - 2 * arr[1] + arr[2])) + return float((arr[0] - arr[2]) / (2 * (arr[0] - 2 * arr[1] + arr[2]))) @staticmethod def _cross_correlation(img0: np.ndarray, diff --git a/src/crappy/tool/image_processing/dis_correl.py b/src/crappy/tool/image_processing/dis_correl.py index 4224b324..6bbf9ea5 100644 --- a/src/crappy/tool/image_processing/dis_correl.py +++ b/src/crappy/tool/image_processing/dis_correl.py @@ -157,7 +157,7 @@ def set_box(self) -> None: # These attributes will be used later self._base = [fields[:, :, :, i] for i in range(fields.shape[3])] - self._norm2 = [np.sum(base_field ** 2) for base_field in self._base] + self._norm2 = [float(np.sum(base_field ** 2)) for base_field in self._base] def get_data(self, img: np.ndarray, @@ -192,12 +192,13 @@ def get_data(self, self._dis_flow = self._dis.calc(self._img0, img, None) # Getting the values to calculate as floats - ret = [np.sum(vec * self._crop(self._dis_flow)) / n2 for vec, n2 in + ret = [float(np.sum(vec * self._crop(self._dis_flow))) / n2 for vec, n2 in zip(self._base, self._norm2)] # Adding the average residual value if requested if residuals: - ret.append(np.average(np.abs(get_res(self._img0, img, self._dis_flow)))) + ret.append(float(np.average(np.abs(get_res(self._img0, img, + self._dis_flow))))) return ret diff --git a/src/crappy/tool/image_processing/video_extenso/video_extenso.py b/src/crappy/tool/image_processing/video_extenso/video_extenso.py index 003aa53e..fe9cf03c 100644 --- a/src/crappy/tool/image_processing/video_extenso/video_extenso.py +++ b/src/crappy/tool/image_processing/video_extenso/video_extenso.py @@ -243,15 +243,15 @@ def get_data(self, x_top_1, x_bottom_1, y_left_1, y_right_1 = box_1.sorted() x_top_2, x_bottom_2, y_left_2, y_right_2 = box_2.sorted() - box_1.x_start = min(x_top_1 + 1, box_1.x_centroid - 2) - box_1.y_start = min(y_left_1 + 1, box_1.y_centroid - 2) - box_1.x_end = max(x_bottom_1 - 1, box_1.x_centroid + 2) - box_1.y_end = max(y_right_1 - 1, box_1.y_centroid + 2) - - box_2.x_start = min(x_top_2 + 1, box_2.x_centroid - 2) - box_2.y_start = min(y_left_2 + 1, box_2.y_centroid - 2) - box_2.x_end = max(x_bottom_2 - 1, box_2.x_centroid + 2) - box_2.y_end = max(y_right_2 - 1, box_2.y_centroid + 2) + box_1.x_start = min(x_top_1 + 1, int(box_1.x_centroid - 2)) + box_1.y_start = min(y_left_1 + 1, int(box_1.y_centroid - 2)) + box_1.x_end = max(x_bottom_1 - 1, int(box_1.x_centroid + 2)) + box_1.y_end = max(y_right_1 - 1, int(box_1.y_centroid + 2)) + + box_2.x_start = min(x_top_2 + 1, int(box_2.x_centroid - 2)) + box_2.y_start = min(y_left_2 + 1, int(box_2.y_centroid - 2)) + box_2.x_end = max(x_bottom_2 - 1, int(box_2.x_centroid + 2)) + box_2.y_end = max(y_right_2 - 1, int(box_2.y_centroid + 2)) if overlap: self._consecutive_overlaps += 1