diff --git a/src/crappy/blocks/camera.py b/src/crappy/blocks/camera.py index a20a5b9e..bf2a8ae2 100644 --- a/src/crappy/blocks/camera.py +++ b/src/crappy/blocks/camera.py @@ -19,7 +19,32 @@ class Camera(Block): - """""" + """This Block can drive a :class:`~crappy.camera.Camera` object. It can + acquire images, display them and record them. It can only drive one Camera at + once. + + It takes no input :class:`~crappy.links.Link` in a majority of situations, + and never has output Links. Most of the time, this Block is used for + recording to the desired location the images it acquires. Optionally, the + images can also be displayed in a dedicated window. Both of these features + are however optional, and it is possible to acquire images and not do + anything with them. Several options are available for tuning the record and + the display. + + Before a test starts, this Block can also display a + :class:`~crappy.tool.camera_config.CameraConfig` window in which the user can + visualize the acquired images, and interactively tune all the + :class:`~crappy.camera.meta_camera.camera_setting.CameraSetting` available + for the instantiated :class:`~crappy.camera.Camera`. + + Internally, this Block is only in charge of the image acquisition, and the + other tasks are parallelized and delegated to + :class:`~crappy.blocks.camera_processes.CameraProcess` objects. The display + is handled by the :class:`~crappy.blocks.camera_processes.Displayer`, and + the recording by the :class:`~crappy.blocks.camera_processes.ImageSaver`. + This Block manages the instantiation, the synchronisation and the + termination of all the CameraProcess it controls. + """ cam_count = dict() @@ -44,7 +69,128 @@ def __init__(self, img_shape: Optional[Tuple[int, int]] = None, img_dtype: Optional[str] = None, **kwargs) -> None: - """""" + """Sets the arguments and initializes the parent class. + + Args: + camera: The name of the :class:`~crappy.camera.Camera` object to use for + acquiring the images. Arguments can be passed to this Camera as + ``kwargs`` of this Block. This argument is ignored if the + ``image_generator`` argument is provided. + transform: A callable taking an image as an argument, and returning a + transformed image as an output. Allows applying a post-processing + operation to the acquired images. This is done right after the + acquisition, so the original image is permanently lost and only the + transformed image is displayed and/or saved and/or further processed. + The transform operation is not parallelized, so it might negatively + affect the acquisition framerate if it is too heavy. + config: If :obj:`True`, a + :class:`~crappy.tool.camera_config.CameraConfig` window is displayed + before the test starts. There, the user can interactively adjust the + different + :class:`~crappy.camera.meta_camera.camera_setting.CameraSetting` + available for the selected :class:`~crappy.camera.Camera`, and + visualize the acquired images. The test starts when closing the + configuration window. If not enabled, the ``img_dtype`` and + ``img_shape`` arguments must be provided. + display_images: If :obj:`True`, displays the acquired images in a + dedicated window, using the backend given in ``displayer_backend`` and + at the frequency specified in ``displayer_framerate``. This option + should be considered as a debug or basic follow-up feature, it is not + intended to be very fast nor to display high-quality images. The + maximum resolution of the displayed images in `640x480`, the images + might be downscaled to fit in this format. + displayer_backend: The backend to use for displaying the images. Can be + either ``'cv2'`` or ``'mpl'``, to use respectively :mod:`cv2` (OpenCV) + or :mod:`matplotlib`. ``'cv2'`` usually allows achieving a higher + display frequency. Ignored if ``display_images`` is :obj:`False`. If + not given and ``display_images`` is :obj:`True`, ``'cv2'`` is tried + first and ``'mpl'`` second, and the first available one is used. + displayer_framerate: The maximum update frequency of the image displayer, + as an :obj:`int`. This value usually lies between 5 and 30Hz, the + default is 5. The achieved update frequency might be lower than + requested. Ignored if ``display_images`` is :obj:`False`. + software_trig_label: The name of a label used as a software trigger for + the :class:`~crappy.camera.Camera`. If given, images will only be + acquired when receiving data over this label. The received value does + not matter. This software trigger is not meant to be very precise, it + is recommended not to rely on it for a trigger frequency greater than + 10Hz, in which case a hardware trigger should be preferred if available + on the camera. + display_freq: If :obj:`True`, displays the looping frequency of the + Block. + debug: If :obj:`True`, displays all the log messages including the + :obj:`~logging.DEBUG` ones. If :obj:`False`, only displays the log + messages with :obj:`~logging.INFO` level or higher. If :obj:`None`, + disables logging for this Block. + freq: The target looping frequency for the Block. If :obj:`None`, loops + as fast as possible. + save_images: If :obj:`True`, the acquired images are saved to the folder + specified in ``save_folder``, in the format specified in + ``img_extension``, using the backend specified in ``save_backend``, and + at the frequency specified in ``save_period``. Each image is saved with + the name : ``_.``, and can thus easily + be identified. Along with the images, a ``metadata.csv`` file records + the metadata of all the saved images. This metadata is either the one + returned by the :meth:`~crappy.camera.Camera.get_image` method of the + :class:`~crappy.camera.Camera` object, or the default one generated in + the :meth:`loop` method of this Block. Depending on the framerate of + the camera and the performance of the computer, it is not guaranteed + that all the acquired images will be recorded. + img_extension: The file extension for the recorded images, as a + :obj:`str` and without the dot. Common file extensions include `tiff`, + `png`, `jpg`, etc. Depending on the used ``save_backend``, some + extensions might not be available. It is currently not possible to + customize the save parameters further than choosing the file extension. + Ignored if ``save_images`` is :obj:`False`. + save_folder: Path to the folder where to save the images, either as a + :obj:`str` or as a :obj:`pathlib.Path`. Can be an absolute or a + relative path, pointing to a folder. If the folder does not exist, it + will be created (if the user has permission). If the given folder + already contains a ``metadata.csv`` file (and thus likely images from + Crappy), images are saved to another folder with the same name except + a suffix is appended. Ignored if ``save_images`` is :obj:`False`. If + not provided and ``save_images`` is :obj:`True`, the images are saved + to the folder ``Crappy_images``, created next to the running script. + save_period: Must be given as an :obj:`int`. Only one out of that number + images at most will be saved. Allows to have a known periodicity in + case the framerate is too high to record all the images. Or simply to + reduce the number of recorded images if saving them all is not needed. + Ignored if ``save_images`` is :obj:`False`. + 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' + + They correspond to the modules :mod:`SimpleITK`, :mod:`cv2` (OpenCV), + :mod:`PIL` (Pillow Fork), 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. + image_generator: A callable taking two :obj:`float` as arguments and + returning an image as a :obj:`numpy.array`. **This argument is intended + for use in the examples of Crappy, to apply an artificial strain on a + base image. Most users should ignore it.** When given, the ``camera`` + argument is ignored and the images are acquired from the generator. To + apply a strain on the image, strain values (in `%`) should be sent to + the Camera Block over the labels ``'Exx(%)'`` and ``'Eyy(%)'``. + img_shape: The shape of the images returned by the + :class:`~crappy.camera.Camera` object as a :obj:`tuple` of :obj:`int`. + It should correspond to the value returned by :obj:`numpy.shape`. + **This argument is mandatory in case** ``config`` **is** :obj:`False`. + It is otherwise ignored. + img_dtype: The `dtype` of the images returned by the + :class:`~crappy.camera.Camera` object, as a :obj:`str`. It should + correspond to a valid data type in :mod:`numpy`, e.g. ``'uint8'``. + **This argument is mandatory in case** ``config`` **is** :obj:`False`. + It is otherwise ignored. + **kwargs: Any additional argument will be passed to the + :class:`~crappy.camera.Camera` object, and used as a kwarg to its + :meth:`~crappy.camera.Camera.open` method. + """ self._save_proc: Optional[ImageSaver] = None self._display_proc: Optional[Displayer] = None @@ -85,7 +231,7 @@ def __init__(self, self._img_dtype = img_dtype self._camera_kwargs = kwargs - # The objects must be initialized later for Windows compatibility + # The synchronization objects are initialized later self._img_array: Optional[SynchronizedArray] = None self._img: Optional[np.ndarray] = None self._metadata: Optional[managers.DictProxy] = None @@ -101,7 +247,7 @@ def __init__(self, self._fps_count = 0 self._last_cam_fps = time() - # Cannot start process from __main__ + # Instantiating the ImageSaver if requested if not save_images: self._save_proc_kw = None else: @@ -110,7 +256,7 @@ def __init__(self, save_period=save_period, save_backend=save_backend) - # Instantiating the displayer window if requested + # Instantiating the Displayer window if requested if not display_images: self._display_proc_kw = None else: @@ -120,7 +266,12 @@ def __init__(self, framerate=displayer_framerate, backend=displayer_backend) def __del__(self) -> None: - """""" + """Safety method called when deleting the Block and ensuring that all the + instantiated :class:`~crappy.blocks.camera_processes.CameraProcess` as well + as the :obj:`~multiprocessing.Manager` are stopped before exiting. + + If they did not stop in time, just terminates them. + """ if self._process_proc is not None and self._process_proc.is_alive(): self._process_proc.terminate() @@ -136,9 +287,13 @@ def __del__(self) -> None: def prepare(self) -> None: """Preparing the save folder, opening the camera and displaying the - configuration GUI.""" + configuration GUI. + + This method calls the :meth:`crappy.camera.Camera.open` method of the + :class:`~crappy.camera.Camera` object. + """ - # Instantiating the multiprocessing objects + # Instantiating the synchronization objects self.log(logging.DEBUG, "Instantiating the multiprocessing " "synchronization objects") self._manager = Manager() @@ -149,6 +304,7 @@ def prepare(self) -> None: self._disp_lock = RLock() self._proc_lock = RLock() + # instantiating the ImageSaver CameraProcess if self._save_proc_kw is not None: self.log(logging.INFO, "Instantiating the saver process") self._save_proc = ImageSaver(log_queue=self._log_queue, @@ -156,6 +312,7 @@ def prepare(self) -> None: display_freq=self.display_freq, **self._save_proc_kw) + # instantiating the Displayer CameraProcess if self._display_proc_kw is not None: self.log(logging.INFO, "Instantiating the displayer process") self._display_proc = Displayer(log_queue=self._log_queue, @@ -163,17 +320,17 @@ def prepare(self) -> None: display_freq=self.display_freq, **self._display_proc_kw) - # Creating the barrier for camera processes synchronization + # Creating the Barrier for the synchronization of the CameraProcesses n_proc = sum(int(proc is not None) for proc in (self._process_proc, self._save_proc, self._display_proc)) if not n_proc: - self.log(logging.WARNING, "The block acquires images but does not save " + self.log(logging.WARNING, "The Block acquires images but does not save " "them, nor display them, nor process them !") self._cam_barrier = Barrier(n_proc + 1) - # Case when the images are generated and not acquired + # Case when the images are artificially generated and not acquired if self._image_generator is not None: self.log(logging.INFO, "Setting the image generator camera") self._camera = BaseCam() @@ -184,18 +341,22 @@ def prepare(self) -> None: self._camera.set_all() def get_image(self_) -> (float, np.ndarray): + """Method generating the frames using the ``image_generator`` argument + if one was provided.""" + return time(), self_.apply_soft_roi(self._image_generator(self_.Exx, self_.Eyy)) self._camera.get_image = MethodType(get_image, self._camera) - # Case when an actual camera object is responsible for acquiring the images + # Instantiating the Camera object for acquiring the images else: self._camera = camera_dict[self._camera_name]() self.log(logging.INFO, f"Opening the {self._camera_name} Camera") self._camera.open(**self._camera_kwargs) self.log(logging.INFO, f"Opened the {self._camera_name} Camera") + # Displaying the configuration window if required if self._config_cam: self.log(logging.INFO, "Displaying the configuration window") self._configure() @@ -208,18 +369,21 @@ def get_image(self_) -> (float, np.ndarray): self.log(logging.INFO, "Setting the trigger mode to Hardware") setattr(self._camera, self._camera.trigger_name, 'Hardware') + # Ensuring a dtype and a shape were given for the image if self._img_dtype is None or self._img_shape is None: raise ValueError(f"Cannot launch the Camera processes for camera " f"{self._camera_name} as the image shape and/or dtype " f"wasn't specified.\n Please specify it in the args, or" f" enable the configuration window.") + # Instantiating the Array for sharing the frames with the CameraProcesses self.log(logging.DEBUG, "Instantiating the shared objects") self._img_array = Array(np.ctypeslib.as_ctypes_type(self._img_dtype), int(np.prod(self._img_shape))) self._img = np.frombuffer(self._img_array.get_obj(), dtype=self._img_dtype).reshape(self._img_shape) + # Starting the CameraProcess for image processing if it was instantiated if self._process_proc is not None: self.log(logging.DEBUG, "Sharing the synchronization objects with the " "image processing process") @@ -237,6 +401,7 @@ def get_image(self_) -> (float, np.ndarray): self.log(logging.INFO, "Starting the image processing process") self._process_proc.start() + # Starting the ImageSaver CameraProcess if it was instantiated if self._save_proc is not None: self.log(logging.DEBUG, "Sharing the synchronization objects with the " "image saver process") @@ -253,6 +418,7 @@ def get_image(self_) -> (float, np.ndarray): self.log(logging.INFO, "Starting the image saver process") self._save_proc.start() + # Starting the Displayer CameraProcess if it was instantiated if self._display_proc is not None: self.log(logging.DEBUG, "Sharing the synchronization objects with the " "image displayer process") @@ -270,7 +436,14 @@ def get_image(self_) -> (float, np.ndarray): self._display_proc.start() def begin(self) -> None: - """""" + """This method waits for all the + :class:`~crappy.blocks.camera_processes.CameraProcess` to be ready, then + releases them all at once to make sure they're synchronized. + + + A :obj:`~multiprocessing.Barrier` is used for forcing the CameraProcesses + to wait for each other. + """ try: self.log(logging.INFO, "Waiting for all Camera processes to be ready") @@ -282,21 +455,35 @@ def begin(self) -> None: self._last_cam_fps = time() def loop(self) -> None: - """Receives the incoming data, acquires an image, displays it, saves it, - and finally processes it if needed.""" - + """This method receives data from upstream Blocks, acquires a frame from + the :class:`~crappy.camera.Camera` object, and transmits it to all the + :class:`~crappy.blocks.camera_processes.CameraProcess`. + + The image is acquired by calling the + :meth:`~crappy.camera.Camera.get_image` method of the Camera object. If + only a timestamp is returned by this method, and not a complete :obj:`dict` + of metadata, some basic metadata is generated here and transmitted to the + CameraProcesses. + + This method also manages the software trigger if this option was set, + applies the image transformation function if one was given, and displays + the FPS of the acquisition if required. + """ + + # Signaling all the Blocks to stop if a CameraProcess crashed if self._stop_event_cam.is_set(): raise CameraRuntimeError + # Receiving the data from upstream Blocks data = self.recv_last_data(fill_missing=False) - # Waiting for the trig label if it was given + # Waiting for the trig label if one was given if self._trig_label is not None and self._trig_label not in data: return elif self._trig_label is not None and self._trig_label in data: self.log(logging.DEBUG, "Software trigger signal received") - # Updating the image generator if there's one + # Updating the image generator if one was provided if self._image_generator is not None: if 'Exx(%)' in data: self.log(logging.DEBUG, f"Setting Exx to {data['Exx(%)']}") @@ -305,13 +492,13 @@ def loop(self) -> None: self.log(logging.DEBUG, f"Setting Eyy to {data['Eyy(%)']}") self._camera.Eyy = data['Eyy(%)'] - # Actually getting the image from the camera object + # Grabbing the frame from the Camera object ret = self._camera.get_image() if ret is None: return metadata, img = ret - - # Building the metadata if it was not provided + + # Building the metadata dict if it was not provided if isinstance(metadata, float): metadata = {'t(s)': metadata, 'DateTimeOriginal': strftime("%Y:%m:%d %H:%M:%S", @@ -319,12 +506,16 @@ def loop(self) -> None: 'SubsecTimeOriginal': f'{metadata % 1:.6f}', 'ImageUniqueID': self._loop_count} + # Making the timestamp relative to the beginning of the test metadata['t(s)'] -= self.t0 - # Applying the transform function + # Applying the transform function if one as provided if self._transform is not None: img = self._transform(img) + # Copying the metadata and the acquired frame into the shared objects for + # transfer to the CameraProcesses + # This is done with all the Locks acquired to avoid any conflict with self._save_lock, self._disp_lock, self._proc_lock: self.log(logging.DEBUG, f"Writing metadata to shared dict: {metadata}") self._metadata.update(metadata) @@ -333,6 +524,7 @@ def loop(self) -> None: self._loop_count += 1 + # If requested, displays the FPS of the image acquisition if self.display_freq: self._fps_count += 1 t = time() @@ -343,41 +535,70 @@ def loop(self) -> None: self._fps_count = 0 def finish(self) -> None: - """""" - + """This method stops the image acquisition on the + :class:`~crappy.camera.Camera`, as well as all the + :class:`~crappy.blocks.camera_processes.CameraProcess` that were started. + + If the CameraProcesses do not gently stop, they are terminated. Also stops + the :obj:`~multiprocessing.Manager` in charge of handling the metadata. + + For stopping the image acquisition, the :meth:`~crappy.camera.Camera.close` + method is called. + """ + + # Closing the Camera object if self._image_generator is None and self._camera is not None: self.log(logging.INFO, f"Closing the {self._camera_name} Camera") self._camera.close() self.log(logging.INFO, f"Closed the {self._camera_name} Camera") + # Setting the stop event to signal all CameraProcesses to stop if self._stop_event_cam is not None: self.log(logging.DEBUG, "Asking all the children processes to stop") self._stop_event_cam.set() sleep(0.2) + # If the processing CameraProcess is not done, terminating it if self._process_proc is not None and self._process_proc.is_alive(): self.log(logging.WARNING, "Image processing process not stopped, " "killing it !") self._process_proc.terminate() + # If the ImageSaver CameraProcess is not done, terminating it if self._save_proc is not None and self._save_proc.is_alive(): self.log(logging.WARNING, "Image saver process not stopped, " "killing it !") self._save_proc.terminate() + # If the Displayer CameraProcess is not done, terminating it if self._display_proc is not None and self._display_proc.is_alive(): self.log(logging.WARNING, "Image displayer process not stopped, " "killing it !") self._display_proc.terminate() + # Closing the Manager handling the metadata if self._manager is not None: self._manager.shutdown() def _configure(self) -> None: - """""" + """This method should instantiate and start the + :class:`~crappy.tool.camera_config.CameraConfig` window for configuring the + :class:`~crappy.camera.Camera` object. + + It should also handle the case when an exception is raised in the + configuration window. + + This method is meant to be overriden by children of the Camera Block, as + other image processing Blocks rely on subclasses of + :class:`~crappy.tool.camera_config.CameraConfig`. + """ config = None + + # Instantiating and starting the configuration window try: config = CameraConfig(self._camera, self._log_queue, self._log_level) config.main() + + # If an exception is raised in the config window, closing it before raising except (Exception,) as exc: self._logger.exception("Caught exception in the configuration window !", exc_info=exc) @@ -385,6 +606,7 @@ def _configure(self) -> None: config.stop() raise CameraConfigError + # Getting the image dtype and shape for setting the shared Array if config.shape is not None: self._img_shape = config.shape if config.dtype is not None: diff --git a/src/crappy/blocks/dic_ve.py b/src/crappy/blocks/dic_ve.py index ab544cec..dfd02095 100644 --- a/src/crappy/blocks/dic_ve.py +++ b/src/crappy/blocks/dic_ve.py @@ -11,7 +11,41 @@ class DICVE(Camera): - """""" + """This Block can perform video-extensometry on images acquired by a + :class:`~crappy.camera.Camera` object, by tracking patches using Digital + Image Correlation techniques. + + It takes no input :class:`~crappy.links.Link` in a majority of situations, + and outputs the results of the video-extensometry. It is a subclass of the + :class:`~crappy.blocks.Camera` Block, and inherits of all its features. That + includes the possibility to record and to display images in real-time, + simultaneously to the image acquisition and processing. Refer to the + documentation of the Camera Block for more information on these features. + + This Block is quite similar to the :class:`~crappy.blocks.VideoExtenso` + Block, except this latter tracks spots instead of patches with a texture. + Both Blocks output similar information, although the default labels and the + data format are slightly different. The :class:`~crappy.blocks.DISCorrel` + Block also relies on image correlation techniques for estimating the + strain and the displacement on acquired images, but it only performs + correlation on a single patch and is designed to have a much greater + accuracy on this single patch. The :class:`~crappy.blocks.GPUVE` Block also + performs video-extensometry based on digital image correlation, but the + correlation is GPU-accelerated. The algorithm used for the correlation is + also different from the ones available in this Block. + + For tracking the provided patches, several image correlation techniques are + available. The most effective one is DISFlow, for which many parameters can + be tuned. The other techniques are lighter on the CPU but also less precise. + For each image, several values are computed and sent to the downstream + Blocks. See the ``labels`` argument for a complete list. + + Similar to the :class:`~crappy.tool.camera_config.CameraConfig` window that + can be displayed by the Camera Block, this Block can display a + :class:`~crappy.tool.camera_config.DICVEConfig` window before the test + starts. Here, the user can also select the patches to track if they were not + already specified as an argument. + """ def __init__(self, camera: str, @@ -49,7 +83,211 @@ def __init__(self, follow: bool = True, raise_on_patch_exit: bool = True, **kwargs) -> None: - """""" + """Sets the arguments and initializes the parent class. + + Args: + camera: The name of the :class:`~crappy.camera.Camera` object to use for + acquiring the images. Arguments can be passed to this Camera as + ``kwargs`` of this Block. This argument is ignored if the + ``image_generator`` argument is provided. + transform: A callable taking an image as an argument, and returning a + transformed image as an output. Allows applying a post-processing + operation to the acquired images. This is done right after the + acquisition, so the original image is permanently lost and only the + transformed image is displayed and/or saved and/or further processed. + The transform operation is not parallelized, so it might negatively + affect the acquisition framerate if it is too heavy. + config: If :obj:`True`, a + :class:`~crappy.tool.camera_config.DICVEConfig` window is displayed + before the test starts. There, the user can interactively adjust the + different + :class:`~crappy.camera.meta_camera.camera_setting.CameraSetting` + available for the selected :class:`~crappy.camera.Camera`, visualize + the acquired images, and select the patches to track if they haven't + been given in the ``patches`` argument. The test starts when closing + the configuration window. If not enabled, the ``img_dtype``, + ``img_shape`` and ``patches`` arguments must be provided. + display_images: If :obj:`True`, displays the acquired images in a + dedicated window, using the backend given in ``displayer_backend`` and + at the frequency specified in ``displayer_framerate``. This option + should be considered as a debug or basic follow-up feature, it is not + intended to be very fast nor to display high-quality images. The + maximum resolution of the displayed images in `640x480`, the images + might be downscaled to fit in this format. In addition to the acquired + frames, the tracked patches are also displayed on the image as an + overlay. + displayer_backend: The backend to use for displaying the images. Can be + either ``'cv2'`` or ``'mpl'``, to use respectively :mod:`cv2` (OpenCV) + or :mod:`matplotlib`. ``'cv2'`` usually allows achieving a higher + display frequency. Ignored if ``display_images`` is :obj:`False`. If + not given and ``display_images`` is :obj:`True`, ``'cv2'`` is tried + first and ``'mpl'`` second, and the first available one is used. + displayer_framerate: The maximum update frequency of the image displayer, + as an :obj:`int`. This value usually lies between 5 and 30Hz, the + default is 5. The achieved update frequency might be lower than + requested. Ignored if ``display_images`` is :obj:`False`. + software_trig_label: The name of a label used as a software trigger for + the :class:`~crappy.camera.Camera`. If given, images will only be + acquired when receiving data over this label. The received value does + not matter. This software trigger is not meant to be very precise, it + is recommended not to rely on it for a trigger frequency greater than + 10Hz, in which case a hardware trigger should be preferred if available + on the camera. + display_freq: If :obj:`True`, displays the looping frequency of the + Block. + debug: If :obj:`True`, displays all the log messages including the + :obj:`~logging.DEBUG` ones. If :obj:`False`, only displays the log + messages with :obj:`~logging.INFO` level or higher. If :obj:`None`, + disables logging for this Block. + freq: The target looping frequency for the Block. If :obj:`None`, loops + as fast as possible. + save_images: If :obj:`True`, the acquired images are saved to the folder + specified in ``save_folder``, in the format specified in + ``img_extension``, using the backend specified in ``save_backend``, and + at the frequency specified in ``save_period``. Each image is saved with + the name : ``_.``, and can thus easily + be identified. Along with the images, a ``metadata.csv`` file records + the metadata of all the saved images. This metadata is either the one + returned by the :meth:`~crappy.camera.Camera.get_image` method of the + :class:`~crappy.camera.Camera` object, or the default one generated in + the :meth:`~crappy.blocks.Camera.loop` method of the + :class:`~crappy.blocks.Camera` Block. Depending on the framerate of the + camera and the performance of the computer, it is not guaranteed that + all the acquired images will be recorded. + img_extension: The file extension for the recorded images, as a + :obj:`str` and without the dot. Common file extensions include `tiff`, + `png`, `jpg`, etc. Depending on the used ``save_backend``, some + extensions might not be available. It is currently not possible to + customize the save parameters further than choosing the file extension. + Ignored if ``save_images`` is :obj:`False`. + save_folder: Path to the folder where to save the images, either as a + :obj:`str` or as a :obj:`pathlib.Path`. Can be an absolute or a + relative path, pointing to a folder. If the folder does not exist, it + will be created (if the user has permission). If the given folder + already contains a ``metadata.csv`` file (and thus likely images from + Crappy), images are saved to another folder with the same name except + a suffix is appended. Ignored if ``save_images`` is :obj:`False`. If + not provided and ``save_images`` is :obj:`True`, the images are saved + to the folder ``Crappy_images``, created next to the running script. + save_period: Must be given as an :obj:`int`. Only one out of that number + images at most will be saved. Allows to have a known periodicity in + case the framerate is too high to record all the images. Or simply to + reduce the number of recorded images if saving them all is not needed. + Ignored if ``save_images`` is :obj:`False`. + 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' + + They correspond to the modules :mod:`SimpleITK`, :mod:`cv2` (OpenCV), + :mod:`PIL` (Pillow Fork), 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. + image_generator: A callable taking two :obj:`float` as arguments and + returning an image as a :obj:`numpy.array`. **This argument is intended + for use in the examples of Crappy, to apply an artificial strain on a + base image. Most users should ignore it.** When given, the ``camera`` + argument is ignored and the images are acquired from the generator. To + apply a strain on the image, strain values (in `%`) should be sent to + the Camera Block over the labels ``'Exx(%)'`` and ``'Eyy(%)'``. + img_shape: The shape of the images returned by the + :class:`~crappy.camera.Camera` object as a :obj:`tuple` of :obj:`int`. + It should correspond to the value returned by :obj:`numpy.shape`. + **This argument is mandatory in case** ``config`` **is** :obj:`False`. + It is otherwise ignored. + img_dtype: The `dtype` of the images returned by the + :class:`~crappy.camera.Camera` object, as a :obj:`str`. It should + correspond to a valid data type in :mod:`numpy`, e.g. ``'uint8'``. + **This argument is mandatory in case** ``config`` **is** :obj:`False`. + It is otherwise ignored. + patches: The coordinates of the several patches to track, as an iterable + (like a :obj:`list` or a :obj:`tuple`) containing one or several + :obj:`tuple` of exactly :obj:`int` values. These integers correspond to + the `y` position of the top-left corner of the patch, the `x` position + of the top-left corner of the patch, the height of the patch, and the + width of the patch. Up to 4 patches can be given and tracked. This + argument must be provided if ``config`` is :obj:`False`. + labels: The labels to use for sending data to downstream Blocks. If not + given, the default labels are + ``'t(s)', 'meta', 'Coord(px)', 'Eyy(%)', 'Exx(%)', 'Disp(px)'``. They + carry for each image its timestamp, a :obj:`dict` containing its + metadata, a :obj:`list` containing for each patch the coordinates of + its center in a :obj:`tuple` of :obj:`int`, the `y` and `x` strain + values calculated from the displacement and the initial position of the + patches, and finally a :obj:`list` containing for each patch its + displacement in the `y` and `x` direction in a :obj:`tuple` of + :obj:`int`. If different labels are desired, they should all be + provided at once in an iterable of :obj:`str` containing the correct + number of labels (6). + method: The method to use for performing the digital image correlation. + Should be one of : + :: + + `Disflow`, 'Pixel precision', 'Parabola', 'Lucas Kanade' + + ``'Disflow'`` uses OpenCV's DISOpticalFlow and ``'Lucas Kanade'`` uses + OpenCV's calcOpticalFlowPyrLK, while all other methods are based on a + basic cross-correlation in the Fourier domain. ``'Pixel precision'`` + calculates the displacement by getting the position of the maximum of + the cross-correlation, and has thus a 1-pixel resolution. It is mainly + meant for debugging. ``'Parabola'`` refines the result of + ``'Pixel precision'`` by interpolating the neighborhood of the maximum, + and has thus a sub-pixel resolution. + alpha: Weight of the smoothness term in DISFlow, as a :obj:`float`. + Ignored if ``method`` is not ``'Disflow'``. + delta: Weight of the color constancy term in DISFlow, as a :obj:`float`. + Ignored if ``method`` is not ``'Disflow'``. + gamma: Weight of the gradient constancy term in DISFlow , as a + :obj:`float`. Ignored if ``method`` is not ``'Disflow'``. + finest_scale: Finest level of the Gaussian pyramid on which the flow is + computed in DISFlow, as an :obj:`int`. Zero level corresponds to the + original image resolution. The final flow is obtained by bilinear + upscaling. Ignored if ``method`` is not ``'Disflow'``. + iterations: The number of fixed point iterations of variational + refinement per scale in DISFlow, as an :obj:`int`. Set to zero to + disable variational refinement completely. Higher values will typically + result in more smooth and high-quality flow. Ignored if ``method`` is + not ``'Disflow'``. + gradient_iterations: The maximum number of gradient descent iterations in + the patch inverse search stage in DISFlow, as an :obj:`int`. Higher + values may improve the quality. Ignored if ``method`` is not + ``'Disflow'``. + patch_size: The size of an image patch for matching in DISFlow, in pixels + as an :obj:`int`. Ignored if ``method`` is not ``'Disflow'``. + patch_stride: The stride between two neighbor patches in DISFlow, in + pixels as an :obj:`int`. Must be less than the ``patch_size``. Lower + values correspond to higher flow quality. Ignored if ``method`` is not + ``'Disflow'``. + border: The ratio of the patch that is kept for calculating the + displacement, if ``method`` is ``'Disflow'``. For example if a value + of `0.2` is given, only the center `80%` of the image is used for + calculating the average displacement, in both directions. Ignored if + ``method`` is not ``'Disflow'``. + safe: If :obj:`True`, checks at each new image if the patches are not + exiting the frame. Otherwise, the patches might exit the image which + can lead to an unexpected behavior without raising an error. + follow: If :obj:`True`, the position of each patch on the images is + adjusted at each new image based on the previous computed displacement + of this patch. If a displacement of 1 in the `x` direction was + calculated on the previous image, and the patch is located at position + `(x0, y0)`, the patch will be moved to position `(x0 + 1, y0)` for the + next image. It "follows" the texture to track. Recommended if the + expected displacement in pixels is big compared to the patch size. The + only downside is that the patches may exit the frame if something goes + wrong with the tracking. + raise_on_patch_exit: If :obj:`True`, raises an exception when a tracked + patch exits the border of the image, which stops the entire test. + Otherwise, just logs a warning message and sleeps until the test is + stopped in another way. + **kwargs: Any additional argument will be passed to the + :class:`~crappy.camera.Camera` object, and used as a kwarg to its + :meth:`~crappy.camera.Camera.open` method. + """ if not config and patches is None: raise ValueError("If the config window is disabled, patches must be " @@ -94,6 +332,7 @@ def __init__(self, self._raise_on_exit = raise_on_patch_exit self._patches_int = list(patches) if patches is not None else None + # These arguments or for the DICVEProcess self._dic_ve_kw = dict(method=method, alpha=alpha, delta=delta, @@ -109,13 +348,21 @@ def __init__(self, raise_on_exit=raise_on_patch_exit) def prepare(self) -> None: - """""" + """This method mostly calls the :meth:`~crappy.blocks.Camera.prepare` + method of the parent class. + + In addition to that is instantiates the + :class:`~crappy.blocks.camera_processes.DICVEProcess` object that performs + the image correlation and the tracking. + """ + # Instantiating the SpotsBoxes containing the patches to track self._patches = SpotsBoxes() if self._patches_int is not None: self._patches.set_spots(self._patches_int) self._patches.save_length() + # Instantiating the DICVEProcess self._process_proc = DICVEProcess(log_queue=self._log_queue, log_level=self._log_level, display_freq=self.display_freq, @@ -125,13 +372,23 @@ def prepare(self) -> None: super().prepare() def _configure(self) -> None: - """""" + """This method should instantiate and start the + :class:`~crappy.tool.camera_config.DICVEConfig` window for configuring the + :class:`~crappy.camera.Camera` object. + + It should also handle the case when an exception is raised in the + configuration window. + """ config = None + + # Instantiating and starting the configuration window try: config = DICVEConfig(self._camera, self._log_queue, self._log_level, self._patches) config.main() + + # If an exception is raised in the config window, closing it before raising except (Exception,) as exc: self._logger.exception("Caught exception in the configuration window !", exc_info=exc) @@ -139,6 +396,7 @@ def _configure(self) -> None: config.stop() raise CameraConfigError + # Getting the image dtype and shape for setting the shared Array if config.shape is not None: self._img_shape = config.shape if config.dtype is not None: diff --git a/src/crappy/blocks/dis_correl.py b/src/crappy/blocks/dis_correl.py index 2d6e087d..4df413b6 100644 --- a/src/crappy/blocks/dis_correl.py +++ b/src/crappy/blocks/dis_correl.py @@ -1,6 +1,6 @@ # coding: utf-8 -from typing import Optional, Callable, List, Union, Tuple, Iterable +from typing import Optional, Callable, Union, Tuple, Iterable import numpy as np from pathlib import Path @@ -11,7 +11,34 @@ class DISCorrel(Camera): - """""" + """This Block can perform Dense Inverse Search on a sub-frame (patch) of + images acquired by a :class:`~crappy.camera.Camera` object, and project the + result on various fields. + + It is mostly used for computing the displacement and the strain over the + given patch, but other fields are also available. refer to the ``fields`` and + ``labels`` arguments for more details. It relies on OpenCV's DISFlow + algorithm, and offers the possibility to adjust many of its settings. + + This Block takes no input :class:`~crappy.links.Link` in a majority of + situations, and outputs the results of image correlation. It is a subclass of + the :class:`~crappy.blocks.Camera` Block, and inherits of all its features. + That includes the possibility to record and to display images in real-time, + simultaneously to the image acquisition and processing. Refer to the + documentation of the Camera Block for more information on these features. + + This Block is very similar to the :class:`GPUCorrel` Block, except this + latter uses GPU-acceleration to perform the image correlation and does not + use DISFlow. The :class:`~crappy.blocks.DICVE` Block also relies on image + correlation for computing the displacement and strain on images, but it + tracks multiple patches and uses video-extensometry. + + Similar to the :class:`~crappy.tool.camera_config.CameraConfig` window that + can be displayed by the Camera Block, this Block can display a + :class:`~crappy.tool.camera_config.DISCorrelConfig` window before the test + starts. Here, the user can also select the patch to track if it was not + already specified as an argument. + """ def __init__(self, camera: str, @@ -47,7 +74,187 @@ def __init__(self, patch_stride: int = 3, residual: bool = False, **kwargs) -> None: - """""" + """Sets the arguments and initializes the parent class. + + Args: + camera: The name of the :class:`~crappy.camera.Camera` object to use for + acquiring the images. Arguments can be passed to this Camera as + ``kwargs`` of this Block. This argument is ignored if the + ``image_generator`` argument is provided. + transform: A callable taking an image as an argument, and returning a + transformed image as an output. Allows applying a post-processing + operation to the acquired images. This is done right after the + acquisition, so the original image is permanently lost and only the + transformed image is displayed and/or saved and/or further processed. + The transform operation is not parallelized, so it might negatively + affect the acquisition framerate if it is too heavy. + config: If :obj:`True`, a + :class:`~crappy.tool.camera_config.DISCorrelConfig` window is displayed + before the test starts. There, the user can interactively adjust the + different + :class:`~crappy.camera.meta_camera.camera_setting.CameraSetting` + available for the selected :class:`~crappy.camera.Camera`, visualize + the acquired images, and select the patch to track if it hasn't + been given in the ``patch`` argument. The test starts when closing + the configuration window. If not enabled, the ``img_dtype``, + ``img_shape`` and ``patch`` arguments must be provided. + display_images: If :obj:`True`, displays the acquired images in a + dedicated window, using the backend given in ``displayer_backend`` and + at the frequency specified in ``displayer_framerate``. This option + should be considered as a debug or basic follow-up feature, it is not + intended to be very fast nor to display high-quality images. The + maximum resolution of the displayed images in `640x480`, the images + might be downscaled to fit in this format. In addition to the acquired + frames, the tracked patch is also displayed on the image as an + overlay. + displayer_backend: The backend to use for displaying the images. Can be + either ``'cv2'`` or ``'mpl'``, to use respectively :mod:`cv2` (OpenCV) + or :mod:`matplotlib`. ``'cv2'`` usually allows achieving a higher + display frequency. Ignored if ``display_images`` is :obj:`False`. If + not given and ``display_images`` is :obj:`True`, ``'cv2'`` is tried + first and ``'mpl'`` second, and the first available one is used. + displayer_framerate: The maximum update frequency of the image displayer, + as an :obj:`int`. This value usually lies between 5 and 30Hz, the + default is 5. The achieved update frequency might be lower than + requested. Ignored if ``display_images`` is :obj:`False`. + software_trig_label: The name of a label used as a software trigger for + the :class:`~crappy.camera.Camera`. If given, images will only be + acquired when receiving data over this label. The received value does + not matter. This software trigger is not meant to be very precise, it + is recommended not to rely on it for a trigger frequency greater than + 10Hz, in which case a hardware trigger should be preferred if available + on the camera. + display_freq: If :obj:`True`, displays the looping frequency of the + Block. + debug: If :obj:`True`, displays all the log messages including the + :obj:`~logging.DEBUG` ones. If :obj:`False`, only displays the log + messages with :obj:`~logging.INFO` level or higher. If :obj:`None`, + disables logging for this Block. + freq: The target looping frequency for the Block. If :obj:`None`, loops + as fast as possible. + save_images: If :obj:`True`, the acquired images are saved to the folder + specified in ``save_folder``, in the format specified in + ``img_extension``, using the backend specified in ``save_backend``, and + at the frequency specified in ``save_period``. Each image is saved with + the name : ``_.``, and can thus easily + be identified. Along with the images, a ``metadata.csv`` file records + the metadata of all the saved images. This metadata is either the one + returned by the :meth:`~crappy.camera.Camera.get_image` method of the + :class:`~crappy.camera.Camera` object, or the default one generated in + the :meth:`~crappy.blocks.Camera.loop` method of the + :class:`~crappy.blocks.Camera` Block. Depending on the framerate of the + camera and the performance of the computer, it is not guaranteed that + all the acquired images will be recorded. + img_extension: The file extension for the recorded images, as a + :obj:`str` and without the dot. Common file extensions include `tiff`, + `png`, `jpg`, etc. Depending on the used ``save_backend``, some + extensions might not be available. It is currently not possible to + customize the save parameters further than choosing the file extension. + Ignored if ``save_images`` is :obj:`False`. + save_folder: Path to the folder where to save the images, either as a + :obj:`str` or as a :obj:`pathlib.Path`. Can be an absolute or a + relative path, pointing to a folder. If the folder does not exist, it + will be created (if the user has permission). If the given folder + already contains a ``metadata.csv`` file (and thus likely images from + Crappy), images are saved to another folder with the same name except + a suffix is appended. Ignored if ``save_images`` is :obj:`False`. If + not provided and ``save_images`` is :obj:`True`, the images are saved + to the folder ``Crappy_images``, created next to the running script. + save_period: Must be given as an :obj:`int`. Only one out of that number + images at most will be saved. Allows to have a known periodicity in + case the framerate is too high to record all the images. Or simply to + reduce the number of recorded images if saving them all is not needed. + Ignored if ``save_images`` is :obj:`False`. + 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' + + They correspond to the modules :mod:`SimpleITK`, :mod:`cv2` (OpenCV), + :mod:`PIL` (Pillow Fork), 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. + image_generator: A callable taking two :obj:`float` as arguments and + returning an image as a :obj:`numpy.array`. **This argument is intended + for use in the examples of Crappy, to apply an artificial strain on a + base image. Most users should ignore it.** When given, the ``camera`` + argument is ignored and the images are acquired from the generator. To + apply a strain on the image, strain values (in `%`) should be sent to + the Camera Block over the labels ``'Exx(%)'`` and ``'Eyy(%)'``. + img_shape: The shape of the images returned by the + :class:`~crappy.camera.Camera` object as a :obj:`tuple` of :obj:`int`. + It should correspond to the value returned by :obj:`numpy.shape`. + **This argument is mandatory in case** ``config`` **is** :obj:`False`. + It is otherwise ignored. + img_dtype: The `dtype` of the images returned by the + :class:`~crappy.camera.Camera` object, as a :obj:`str`. It should + correspond to a valid data type in :mod:`numpy`, e.g. ``'uint8'``. + **This argument is mandatory in case** ``config`` **is** :obj:`False`. + It is otherwise ignored. + patch: The coordinates of the patch to track, as a :obj:`tuple` of + exactly 4 :obj:`int`. These integers correspond to the `y` position of + the top-left corner of the patch, the `x` position of the top-left + corner of the patch, the height of the patch, and the width of the + patch. Only one patch can be tracked. This argument must be provided if + ``config`` is :obj:`False`. + fields: The several fields to calculate on the acquired images. They + should be given as an iterable containing :obj:`str`. Each string + represents one field to calculate, so the more fields are given the + heavier the computation is. The possible fields are : + :: + + 'x', 'y', 'r', 'exx', 'eyy', 'exy', 'eyx', 'exy2', 'z' + + For each field, one single value is computed, corresponding to the + norm of the field values over the patch area. If not provided, the + default fields are ``'x', 'y', 'exx', 'eyy'``. + labels: The labels to use for sending data to downstream Blocks. If not + given, the default labels are + ``'t(s)', 'meta', 'x(pix)', 'y(pix)', 'Exx(%)', 'Eyy(%)'``. They carry + for each image its timestamp, a :obj:`dict` containing its metadata, + and then for each field the computed value as a :obj:`float`. These + default labels are compatible with the default fields, but must be + changed if the number of fields changes. When setting this argument, + make sure to give at least 2 labels for the time and the metadata, and + one label per field. The ``'res'`` label containing the residuals if + ``residual`` is :obj:`True` should not be included here, it will be + automatically added. + alpha: Weight of the smoothness term in DISFlow, as a :obj:`float`. + delta: Weight of the color constancy term in DISFlow, as a :obj:`float`. + gamma: Weight of the gradient constancy term in DISFlow , as a + :obj:`float`. + finest_scale: Finest level of the Gaussian pyramid on which the flow is + computed in DISFlow, as an :obj:`int`. Zero level corresponds to the + original image resolution. The final flow is obtained by bilinear + upscaling. + iterations: The number of fixed point iterations of variational + refinement per scale in DISFlow, as an :obj:`int`. Set to zero to + disable variational refinement completely. Higher values will typically + result in more smooth and high-quality flow. + gradient_iterations: The maximum number of gradient descent iterations in + the patch inverse search stage in DISFlow, as an :obj:`int`. Higher + values may improve the quality. + init: If :obj:`True`, the last calculated optical flow is used for + initializing the calculation of the next one. + patch_size: The size of an image patch for matching in DISFlow, in pixels + as an :obj:`int`. + patch_stride: The stride between two neighbor patches in DISFlow, in + pixels as an :obj:`int`. Must be less than the ``patch_size``. Lower + values correspond to higher flow quality. + residual: If :obj:`True`, the residuals of the optical flow calculation + are computed for each image. They are then returned under the ``'res'`` + label, that should not be included in the given labels. This option is + mainly intended as a debug feature, to monitor the quality of the + image correlation. + **kwargs: Any additional argument will be passed to the + :class:`~crappy.camera.Camera` object, and used as a kwarg to its + :meth:`~crappy.camera.Camera.open` method. + """ if not config and patch is None: raise ValueError("If the config window is disabled, the patch must be " @@ -115,8 +322,15 @@ def __init__(self, residual=residual) def prepare(self) -> None: - """""" + """This method mostly calls the :meth:`~crappy.blocks.Camera.prepare` + method of the parent class. + + In addition to that is instantiates the + :class:`~crappy.blocks.camera_processes.DISCorrelProcess` object that + performs the image correlation and the tracking. + """ + # Instantiating the Box containing the patch to track if self._patch_int is not None: self._patch = Box(x_start=self._patch_int[1], x_end=self._patch_int[1] + self._patch_int[3], @@ -125,6 +339,7 @@ def prepare(self) -> None: else: self._patch = Box() + # Instantiating the DISCorrelProcess self._process_proc = DISCorrelProcess(log_queue=self._log_queue, log_level=self._log_level, display_freq=self.display_freq, @@ -134,13 +349,23 @@ def prepare(self) -> None: super().prepare() def _configure(self) -> None: - """""" + """This method should instantiate and start the + :class:`~crappy.tool.camera_config.DISCorrelConfig` window for configuring + the :class:`~crappy.camera.Camera` object. + + It should also handle the case when an exception is raised in the + configuration window. + """ config = None + + # Instantiating and starting the configuration window try: config = DISCorrelConfig(self._camera, self._log_queue, self._log_level, self._patch) config.main() + + # If an exception is raised in the config window, closing it before raising except (Exception,) as exc: self._logger.exception("Caught exception in the configuration window !", exc_info=exc) @@ -148,6 +373,7 @@ def _configure(self) -> None: config.stop() raise CameraConfigError + # Getting the image dtype and shape for setting the shared Array if config.shape is not None: self._img_shape = config.shape if config.dtype is not None: diff --git a/src/crappy/blocks/gpu_correl.py b/src/crappy/blocks/gpu_correl.py index 2a3ffcd6..911bc604 100644 --- a/src/crappy/blocks/gpu_correl.py +++ b/src/crappy/blocks/gpu_correl.py @@ -9,7 +9,32 @@ class GPUCorrel(Camera): - """""" + """This Block can perform GPU-accelerated image correlation on images + acquired by a :class:`~crappy.camera.Camera` object, and project the result + on various fields. + + It is mostly used for computing the displacement and the strain over the + given patch, but other fields are also available. refer to the ``fields`` and + ``labels`` arguments for more details. + + This Block takes no input :class:`~crappy.links.Link` in a majority of + situations, and outputs the results of image correlation. It is a subclass of + the :class:`~crappy.blocks.Camera` Block, and inherits of all its features. + That includes the possibility to record and to display images in real-time, + simultaneously to the image acquisition and processing. Refer to the + documentation of the Camera Block for more information on these features. + + This Block is very similar to the :class:`DISCorrel` Block, except this + latter uses DISFlow to perform the image correlation and is not + GPU-accelerated. The :class:`~crappy.blocks.GPUVE` Block also relies on + GPU-accelerated image correlation for computing the displacement and strain + on images, but it tracks multiple patches and uses video-extensometry. + + No Region Of Interest can be specified to this Block, so by default the + correlation is performed on the entire image. It is however possible to set + a mask, so that only part of the image is considered when running the + correlation. + """ def __init__(self, camera: str, @@ -43,7 +68,181 @@ def __init__(self, mul: float = 3, res: bool = False, **kwargs) -> None: - """""" + """Sets the arguments and initializes the parent class. + + Args: + camera: The name of the :class:`~crappy.camera.Camera` object to use for + acquiring the images. Arguments can be passed to this Camera as + ``kwargs`` of this Block. This argument is ignored if the + ``image_generator`` argument is provided. + fields: The several fields to calculate on the acquired images. They + should be given as an iterable containing :obj:`str`. Each string + represents one field to calculate, so the more fields are given the + heavier the computation is. The possible fields are : + :: + + 'x', 'y', 'r', 'exx', 'eyy', 'exy', 'eyx', 'exy2', 'z' + + For each field, one single value is computed, corresponding to the + norm of the field values over the patch area. + img_shape: The shape of the images returned by the + :class:`~crappy.camera.Camera` object as a :obj:`tuple` of :obj:`int`. + It should correspond to the value returned by :obj:`numpy.shape`. + img_dtype: The `dtype` of the images returned by the + :class:`~crappy.camera.Camera` object, as a :obj:`str`. It should + correspond to a valid data type in :mod:`numpy`, e.g. ``'uint8'``. + transform: A callable taking an image as an argument, and returning a + transformed image as an output. Allows applying a post-processing + operation to the acquired images. This is done right after the + acquisition, so the original image is permanently lost and only the + transformed image is displayed and/or saved and/or further processed. + The transform operation is not parallelized, so it might negatively + affect the acquisition framerate if it is too heavy. + display_images: If :obj:`True`, displays the acquired images in a + dedicated window, using the backend given in ``displayer_backend`` and + at the frequency specified in ``displayer_framerate``. This option + should be considered as a debug or basic follow-up feature, it is not + intended to be very fast nor to display high-quality images. The + maximum resolution of the displayed images in `640x480`, the images + might be downscaled to fit in this format. + displayer_backend: The backend to use for displaying the images. Can be + either ``'cv2'`` or ``'mpl'``, to use respectively :mod:`cv2` (OpenCV) + or :mod:`matplotlib`. ``'cv2'`` usually allows achieving a higher + display frequency. Ignored if ``display_images`` is :obj:`False`. If + not given and ``display_images`` is :obj:`True`, ``'cv2'`` is tried + first and ``'mpl'`` second, and the first available one is used. + displayer_framerate: The maximum update frequency of the image displayer, + as an :obj:`int`. This value usually lies between 5 and 30Hz, the + default is 5. The achieved update frequency might be lower than + requested. Ignored if ``display_images`` is :obj:`False`. + software_trig_label: The name of a label used as a software trigger for + the :class:`~crappy.camera.Camera`. If given, images will only be + acquired when receiving data over this label. The received value does + not matter. This software trigger is not meant to be very precise, it + is recommended not to rely on it for a trigger frequency greater than + 10Hz, in which case a hardware trigger should be preferred if available + on the camera. + verbose: The verbose level as an integer, between `0` and `3`. At level + `0` no information is displayed, and at level `3` so much information + is displayed that is slows the code down. This argument allows to + adjust the precision of the log messages, while the ``debug`` argument + is for enabling or disabling logging. + freq: The target looping frequency for the Block. If :obj:`None`, loops + as fast as possible. + debug: If :obj:`True`, displays all the log messages including the + :obj:`~logging.DEBUG` ones. If :obj:`False`, only displays the log + messages with :obj:`~logging.INFO` level or higher. If :obj:`None`, + disables logging for this Block. + save_images: If :obj:`True`, the acquired images are saved to the folder + specified in ``save_folder``, in the format specified in + ``img_extension``, using the backend specified in ``save_backend``, and + at the frequency specified in ``save_period``. Each image is saved with + the name : ``_.``, and can thus easily + be identified. Along with the images, a ``metadata.csv`` file records + the metadata of all the saved images. This metadata is either the one + returned by the :meth:`~crappy.camera.Camera.get_image` method of the + :class:`~crappy.camera.Camera` object, or the default one generated in + the :meth:`~crappy.blocks.Camera.loop` method of the + :class:`~crappy.blocks.Camera` Block. Depending on the framerate of the + camera and the performance of the computer, it is not guaranteed that + all the acquired images will be recorded. + img_extension: The file extension for the recorded images, as a + :obj:`str` and without the dot. Common file extensions include `tiff`, + `png`, `jpg`, etc. Depending on the used ``save_backend``, some + extensions might not be available. It is currently not possible to + customize the save parameters further than choosing the file extension. + Ignored if ``save_images`` is :obj:`False`. + save_folder: Path to the folder where to save the images, either as a + :obj:`str` or as a :obj:`pathlib.Path`. Can be an absolute or a + relative path, pointing to a folder. If the folder does not exist, it + will be created (if the user has permission). If the given folder + already contains a ``metadata.csv`` file (and thus likely images from + Crappy), images are saved to another folder with the same name except + a suffix is appended. Ignored if ``save_images`` is :obj:`False`. If + not provided and ``save_images`` is :obj:`True`, the images are saved + to the folder ``Crappy_images``, created next to the running script. + save_period: Must be given as an :obj:`int`. Only one out of that number + images at most will be saved. Allows to have a known periodicity in + case the framerate is too high to record all the images. Or simply to + reduce the number of recorded images if saving them all is not needed. + Ignored if ``save_images`` is :obj:`False`. + 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' + + They correspond to the modules :mod:`SimpleITK`, :mod:`cv2` (OpenCV), + :mod:`PIL` (Pillow Fork), 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. + image_generator: A callable taking two :obj:`float` as arguments and + returning an image as a :obj:`numpy.array`. **This argument is intended + for use in the examples of Crappy, to apply an artificial strain on a + base image. Most users should ignore it.** When given, the ``camera`` + argument is ignored and the images are acquired from the generator. To + apply a strain on the image, strain values (in `%`) should be sent to + the Camera Block over the labels ``'Exx(%)'`` and ``'Eyy(%)'``. + labels: The labels to use for sending data to downstream Blocks. If not + given, the default labels are ``'t(s)', 'meta'`` followed by the names + of the given ``fields``. They carry for each image its timestamp, a + :obj:`dict` containing its metadata, and then for each field the + computed value as a :obj:`float`. When setting this argument, make sure + to give at least 2 labels for the time and the metadata, and one label + per field. The ``'res'`` label containing the residuals if ``res`` is + :obj:`True` should not be included here, it will be automatically + added. + discard_limit: If ``res`` is :obj:`True`, the result of the + correlation is not sent to the downstream Blocks if the residuals for + the current image are greater than ``discard_limit`` times the average + residual for the last ``discard_ref`` images. + discard_ref: If ``res`` is :obj:`True`, the result of the + correlation is not sent to the downstream Blocks if the residuals for + the current image are greater than ``discard_limit`` times the average + residual for the last ``discard_ref`` images. + img_ref: A reference image, as a 2D :obj:`numpy.array` with `dtype` + `float32`. If given, it will be set early on the correlation class and + the test can start from the first acquired frame. If not given, the + first acquired image will be set as the reference, which will slow down + the beginning of the test. + levels: Number of levels of the pyramid. More levels may help converging + on images with large strain, but may fail on images that don't contain + low spatial frequency. Fewer levels mean that the program runs faster. + resampling_factor: The factor by which the resolution is divided between + each stage of the pyramid. A low factor ensures coherence between the + stages, but is more computationally intensive. A high factor allows + reaching a finer detail level, but may lead to a coherence loss between + the stages. + kernel_file: The path to the file containing the kernels to use for the + correlation. Can be a :obj:`pathlib.Path` object or a :obj:`str`. If + not provided, the default :ref:`GPU Kernels` are used. + iterations: The maximum number of iterations to run before returning the + results. The results may be returned before if the residuals start + increasing. + mask: The mask used for weighting the region of interest on the image. It + is generally used to prevent unexpected behavior on the border of the + image. Also allows to select a sub-region of the image if the + correlation should not be performed on the entire image, as this Block + does not accept a `patch` argument. + mul: The scalar by which the direction will be multiplied before being + added to the solution. If it's too high, the convergence will be fast + but there's a risk to go past the solution and to diverge. If it's too + low, the convergence will be slower and require more iterations. `3` + was found to be an acceptable value in most cases, but it is + recommended to tune this value for each application so that the + convergence is neither too slow nor too fast. + res: If :obj:`True`, calculates the residuals after performing the + correlation and returns the residuals along with the correlation data. + The residuals are always returned under the label ``'res'``, and this + label should not be included in the ``labels`` argument. + **kwargs: Any additional argument will be passed to the + :class:`~crappy.camera.Camera` object, and used as a kwarg to its + :meth:`~crappy.camera.Camera.open` method. + """ super().__init__(camera=camera, transform=transform, @@ -83,6 +282,7 @@ def __init__(self, self.labels.append('res') self._calc_res = res + # Checking that the number of fields and the number of labels match if 2 + len(fields) + int(res) != len(self.labels): raise ValueError("The number of fields is inconsistent with the number " "of labels !\nMake sure that the time label was given") @@ -101,8 +301,15 @@ def __init__(self, mul=mul) def prepare(self) -> None: - """""" + """This method mostly calls the :meth:`~crappy.blocks.Camera.prepare` + method of the parent class. + + In addition to that is instantiates the + :class:`~crappy.blocks.camera_processes.GPUCorrelProcess` object that + performs the GPU-accelerated image correlation. + """ + # Instantiating the GPUCorrelProcess self._process_proc = GPUCorrelProcess(log_queue=self._log_queue, log_level=self._log_level, **self._gpu_correl_kw) @@ -110,6 +317,7 @@ def prepare(self) -> None: super().prepare() def _configure(self) -> None: - """""" + """No configuration window is available for this Block, so this method was + left blank.""" ... diff --git a/src/crappy/blocks/gpu_ve.py b/src/crappy/blocks/gpu_ve.py index ef7fa828..0c6f2f86 100644 --- a/src/crappy/blocks/gpu_ve.py +++ b/src/crappy/blocks/gpu_ve.py @@ -9,7 +9,26 @@ class GPUVE(Camera): - """""" + """This Block can perform GPU-accelerated video-extensometry on images + acquired by a :class:`~crappy.camera.Camera` object, by tracking patches and + computing the strain based on their displacement. + + It takes no input :class:`~crappy.links.Link` in a majority of situations, + and outputs the results of the video-extensometry. It is a subclass of the + :class:`~crappy.blocks.Camera` Block, and inherits of all its features. That + includes the possibility to record and to display images in real-time, + simultaneously to the image acquisition and processing. Refer to the + documentation of the Camera Block for more information on these features. + + This Block is quite similar to the :class:`~crappy.blocks.DICVE` Block, + except this latter is not GPU-accelerated and uses OpenCV's DISFlow. The + :class:`~crappy.blocks.GPUCorrel` Block also relies on GPU-accelerated image + correlation for estimating the strain and the displacement on acquired + images, but it performs correlation on the entire image and is designed to + have a much greater accuracy. The :class:`~crappy.blocks.VideoExtenso` Block + also performs video-extensometry, but it does so by tracking spots instead + of textured patches, and it is not GPU-accelerated. + """ def __init__(self, camera: str, @@ -37,7 +56,152 @@ def __init__(self, iterations: int = 4, mul: float = 3, **kwargs) -> None: - """""" + """Sets the arguments and initializes the parent class. + + Args: + camera: The name of the :class:`~crappy.camera.Camera` object to use for + acquiring the images. Arguments can be passed to this Camera as + ``kwargs`` of this Block. This argument is ignored if the + ``image_generator`` argument is provided. + patches: The coordinates of the several patches to track, as an iterable + (like a :obj:`list` or a :obj:`tuple`) containing one or several + :obj:`tuple` of exactly :obj:`int` values. These integers correspond to + the `y` position of the top-left corner of the patch, the `x` position + of the top-left corner of the patch, the height of the patch, and the + width of the patch. Up to 4 patches can be given and tracked. + img_shape: The shape of the images returned by the + :class:`~crappy.camera.Camera` object as a :obj:`tuple` of :obj:`int`. + It should correspond to the value returned by :obj:`numpy.shape`. + img_dtype: The `dtype` of the images returned by the + :class:`~crappy.camera.Camera` object, as a :obj:`str`. It should + correspond to a valid data type in :mod:`numpy`, e.g. ``'uint8'``. + transform: A callable taking an image as an argument, and returning a + transformed image as an output. Allows applying a post-processing + operation to the acquired images. This is done right after the + acquisition, so the original image is permanently lost and only the + transformed image is displayed and/or saved and/or further processed. + The transform operation is not parallelized, so it might negatively + affect the acquisition framerate if it is too heavy. + display_images: If :obj:`True`, displays the acquired images in a + dedicated window, using the backend given in ``displayer_backend`` and + at the frequency specified in ``displayer_framerate``. This option + should be considered as a debug or basic follow-up feature, it is not + intended to be very fast nor to display high-quality images. The + maximum resolution of the displayed images in `640x480`, the images + might be downscaled to fit in this format. In addition to the acquired + frames, the tracked patches are also displayed on the image as an + overlay. + displayer_backend: The backend to use for displaying the images. Can be + either ``'cv2'`` or ``'mpl'``, to use respectively :mod:`cv2` (OpenCV) + or :mod:`matplotlib`. ``'cv2'`` usually allows achieving a higher + display frequency. Ignored if ``display_images`` is :obj:`False`. If + not given and ``display_images`` is :obj:`True`, ``'cv2'`` is tried + first and ``'mpl'`` second, and the first available one is used. + displayer_framerate: The maximum update frequency of the image displayer, + as an :obj:`int`. This value usually lies between 5 and 30Hz, the + default is 5. The achieved update frequency might be lower than + requested. Ignored if ``display_images`` is :obj:`False`. + software_trig_label: The name of a label used as a software trigger for + the :class:`~crappy.camera.Camera`. If given, images will only be + acquired when receiving data over this label. The received value does + not matter. This software trigger is not meant to be very precise, it + is recommended not to rely on it for a trigger frequency greater than + 10Hz, in which case a hardware trigger should be preferred if available + on the camera. + verbose: The verbose level as an integer, between `0` and `3`. At level + `0` no information is displayed, and at level `3` so much information + is displayed that is slows the code down. This argument allows to + adjust the precision of the log messages, while the ``debug`` argument + is for enabling or disabling logging. + freq: The target looping frequency for the Block. If :obj:`None`, loops + as fast as possible. + debug: If :obj:`True`, displays all the log messages including the + :obj:`~logging.DEBUG` ones. If :obj:`False`, only displays the log + messages with :obj:`~logging.INFO` level or higher. If :obj:`None`, + disables logging for this Block. + save_images: If :obj:`True`, the acquired images are saved to the folder + specified in ``save_folder``, in the format specified in + ``img_extension``, using the backend specified in ``save_backend``, and + at the frequency specified in ``save_period``. Each image is saved with + the name : ``_.``, and can thus easily + be identified. Along with the images, a ``metadata.csv`` file records + the metadata of all the saved images. This metadata is either the one + returned by the :meth:`~crappy.camera.Camera.get_image` method of the + :class:`~crappy.camera.Camera` object, or the default one generated in + the :meth:`~crappy.blocks.Camera.loop` method of the + :class:`~crappy.blocks.Camera` Block. Depending on the framerate of the + camera and the performance of the computer, it is not guaranteed that + all the acquired images will be recorded. + img_extension: The file extension for the recorded images, as a + :obj:`str` and without the dot. Common file extensions include `tiff`, + `png`, `jpg`, etc. Depending on the used ``save_backend``, some + extensions might not be available. It is currently not possible to + customize the save parameters further than choosing the file extension. + Ignored if ``save_images`` is :obj:`False`. + save_folder: Path to the folder where to save the images, either as a + :obj:`str` or as a :obj:`pathlib.Path`. Can be an absolute or a + relative path, pointing to a folder. If the folder does not exist, it + will be created (if the user has permission). If the given folder + already contains a ``metadata.csv`` file (and thus likely images from + Crappy), images are saved to another folder with the same name except + a suffix is appended. Ignored if ``save_images`` is :obj:`False`. If + not provided and ``save_images`` is :obj:`True`, the images are saved + to the folder ``Crappy_images``, created next to the running script. + save_period: Must be given as an :obj:`int`. Only one out of that number + images at most will be saved. Allows to have a known periodicity in + case the framerate is too high to record all the images. Or simply to + reduce the number of recorded images if saving them all is not needed. + Ignored if ``save_images`` is :obj:`False`. + 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' + + They correspond to the modules :mod:`SimpleITK`, :mod:`cv2` (OpenCV), + :mod:`PIL` (Pillow Fork), 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. + image_generator: A callable taking two :obj:`float` as arguments and + returning an image as a :obj:`numpy.array`. **This argument is intended + for use in the examples of Crappy, to apply an artificial strain on a + base image. Most users should ignore it.** When given, the ``camera`` + argument is ignored and the images are acquired from the generator. To + apply a strain on the image, strain values (in `%`) should be sent to + the Camera Block over the labels ``'Exx(%)'`` and ``'Eyy(%)'``. + labels: The labels to use for sending data to downstream Blocks. If not + given, the default labels are ``'t(s)', 'meta'`` followed for each + given patch by ``'px', 'py'`` with ``''`` the number of the + patch. They carry for each image its timestamp, a :obj:`dict` + containing its metadata, and then for each patch the `x` and `y` + positions of its centroid. When setting this argument, make sure to + give 2 labels for the time and the metadata, and two labels per patch. + img_ref: A reference image, as a 2D :obj:`numpy.array` with `dtype` + `float32`. If given, it will be set early on the correlation class and + the test can start from the first acquired frame. If not given, the + first acquired image will be set as the reference, which will slow down + the beginning of the test. + kernel_file: The path to the file containing the kernels to use for the + correlation. Can be a :obj:`pathlib.Path` object or a :obj:`str`. If + not provided, the default :ref:`GPU Kernels` are used. + iterations: The maximum number of iterations to run before returning the + results. The results may be returned before if the residuals start + increasing. + mul: The scalar by which the direction will be multiplied before being + added to the solution. If it's too high, the convergence will be fast + but there's a risk to go past the solution and to diverge. If it's too + low, the convergence will be slower and require more iterations. `3` + was found to be an acceptable value in most cases, but it is + recommended to tune this value for each application so that the + convergence is neither too slow nor too fast. + **kwargs: Any additional argument will be passed to the + :class:`~crappy.camera.Camera` object, and used as a kwarg to its + :meth:`~crappy.camera.Camera.open` method. + """ super().__init__(camera=camera, transform=transform, @@ -74,8 +238,9 @@ def __init__(self, self._img_ref = img_ref + # Checking that the number of fields and the number of patches match if 2 + 2 * len(patches) != len(self.labels): - raise ValueError("The number of fields is inconsistent with the number " + raise ValueError("The number of patches is inconsistent with the number " "of labels !\nMake sure that the time and metadata " "labels were given") @@ -87,8 +252,15 @@ def __init__(self, mul=mul) def prepare(self) -> None: - """""" + """This method mostly calls the :meth:`~crappy.blocks.Camera.prepare` + method of the parent class. + + In addition to that is instantiates the + :class:`~crappy.blocks.camera_processes.GPUVEProcess` object that + performs the GPU-accelerated image correlation. + """ + # Instantiating the GPUVEProcess self._process_proc = GPUVEProcess(log_queue=self._log_queue, log_level=self._log_level, **self._gpu_ve_kw) @@ -96,6 +268,7 @@ def prepare(self) -> None: super().prepare() def _configure(self) -> None: - """""" + """No configuration window is available for this Block, so this method was + left blank.""" ... diff --git a/src/crappy/blocks/video_extenso.py b/src/crappy/blocks/video_extenso.py index 56b7ed48..92274142 100644 --- a/src/crappy/blocks/video_extenso.py +++ b/src/crappy/blocks/video_extenso.py @@ -11,7 +11,32 @@ class VideoExtenso(Camera): - """""" + """This Block can perform video-extensometry on images acquired by a + :class:`~crappy.camera.Camera` object, by tracking spots on the images. + + It takes no input :class:`~crappy.links.Link` in a majority of situations, + and outputs the results of the video-extensometry. It is a subclass of the + :class:`~crappy.blocks.Camera` Block, and inherits of all its features. That + includes the possibility to record and to display images in real-time, + simultaneously to the image acquisition and processing. Refer to the + documentation of the Camera Block for more information on these features. + + This Block is quite similar to the :class:`~crappy.blocks.DICVE` Block, + except this latter tracks patches with a texture instead of spots. Both + Blocks output similar information, although the default labels and the data + format are slightly different. See the ``labels`` argument for more detail on + the output values. Similar to the DICVE, the :class:`~crappy.blocks.GPUVE` + Block also performs video-extensometry based on GPU-accelerated image + correlation. + + Similar to the :class:`~crappy.tool.camera_config.CameraConfig` window that + can be displayed by the Camera Block, this Block can display a + :class:`~crappy.tool.camera_config.VideoExtensoConfig` window before the test + starts. Here, the user can also detect and select the spots to track. It is + currently not possible to specify the coordinates of the spots to track as an + argument, so the use of the configuration window is mandatory. This might + change in the future. + """ def __init__(self, camera: str, @@ -41,9 +66,179 @@ def __init__(self, safe_mode: bool = False, border: int = 5, min_area: int = 150, - blur: int = 5, + blur: Optional[int] = 5, **kwargs) -> None: - """""" + """Sets the arguments and initializes the parent class. + + Args: + camera: The name of the :class:`~crappy.camera.Camera` object to use for + acquiring the images. Arguments can be passed to this Camera as + ``kwargs`` of this Block. This argument is ignored if the + ``image_generator`` argument is provided. + transform: A callable taking an image as an argument, and returning a + transformed image as an output. Allows applying a post-processing + operation to the acquired images. This is done right after the + acquisition, so the original image is permanently lost and only the + transformed image is displayed and/or saved and/or further processed. + The transform operation is not parallelized, so it might negatively + affect the acquisition framerate if it is too heavy. + config: If :obj:`True`, a + :class:`~crappy.tool.camera_config.VideoExtensoConfig` window is + displayed before the test starts. There, the user can interactively + adjust the different + :class:`~crappy.camera.meta_camera.camera_setting.CameraSetting` + available for the selected :class:`~crappy.camera.Camera`, visualize + the acquired images, and detect and select the spots to track. The test + starts when closing the configuration window. **It is currently not + possible to set this argument to** :obj:`False` **!** This might change + in the future. + display_images: If :obj:`True`, displays the acquired images in a + dedicated window, using the backend given in ``displayer_backend`` and + at the frequency specified in ``displayer_framerate``. This option + should be considered as a debug or basic follow-up feature, it is not + intended to be very fast nor to display high-quality images. The + maximum resolution of the displayed images in `640x480`, the images + might be downscaled to fit in this format. In addition to the acquired + frames, the tracked spots are also displayed on the image as an + overlay. + displayer_backend: The backend to use for displaying the images. Can be + either ``'cv2'`` or ``'mpl'``, to use respectively :mod:`cv2` (OpenCV) + or :mod:`matplotlib`. ``'cv2'`` usually allows achieving a higher + display frequency. Ignored if ``display_images`` is :obj:`False`. If + not given and ``display_images`` is :obj:`True`, ``'cv2'`` is tried + first and ``'mpl'`` second, and the first available one is used. + displayer_framerate: The maximum update frequency of the image displayer, + as an :obj:`int`. This value usually lies between 5 and 30Hz, the + default is 5. The achieved update frequency might be lower than + requested. Ignored if ``display_images`` is :obj:`False`. + software_trig_label: The name of a label used as a software trigger for + the :class:`~crappy.camera.Camera`. If given, images will only be + acquired when receiving data over this label. The received value does + not matter. This software trigger is not meant to be very precise, it + is recommended not to rely on it for a trigger frequency greater than + 10Hz, in which case a hardware trigger should be preferred if available + on the camera. + display_freq: If :obj:`True`, displays the looping frequency of the + Block. + debug: If :obj:`True`, displays all the log messages including the + :obj:`~logging.DEBUG` ones. If :obj:`False`, only displays the log + messages with :obj:`~logging.INFO` level or higher. If :obj:`None`, + disables logging for this Block. + freq: The target looping frequency for the Block. If :obj:`None`, loops + as fast as possible. + save_images: If :obj:`True`, the acquired images are saved to the folder + specified in ``save_folder``, in the format specified in + ``img_extension``, using the backend specified in ``save_backend``, and + at the frequency specified in ``save_period``. Each image is saved with + the name : ``_.``, and can thus easily + be identified. Along with the images, a ``metadata.csv`` file records + the metadata of all the saved images. This metadata is either the one + returned by the :meth:`~crappy.camera.Camera.get_image` method of the + :class:`~crappy.camera.Camera` object, or the default one generated in + the :meth:`~crappy.blocks.Camera.loop` method of the + :class:`~crappy.blocks.Camera` Block. Depending on the framerate of the + camera and the performance of the computer, it is not guaranteed that + all the acquired images will be recorded. + img_extension: The file extension for the recorded images, as a + :obj:`str` and without the dot. Common file extensions include `tiff`, + `png`, `jpg`, etc. Depending on the used ``save_backend``, some + extensions might not be available. It is currently not possible to + customize the save parameters further than choosing the file extension. + Ignored if ``save_images`` is :obj:`False`. + save_folder: Path to the folder where to save the images, either as a + :obj:`str` or as a :obj:`pathlib.Path`. Can be an absolute or a + relative path, pointing to a folder. If the folder does not exist, it + will be created (if the user has permission). If the given folder + already contains a ``metadata.csv`` file (and thus likely images from + Crappy), images are saved to another folder with the same name except + a suffix is appended. Ignored if ``save_images`` is :obj:`False`. If + not provided and ``save_images`` is :obj:`True`, the images are saved + to the folder ``Crappy_images``, created next to the running script. + save_period: Must be given as an :obj:`int`. Only one out of that number + images at most will be saved. Allows to have a known periodicity in + case the framerate is too high to record all the images. Or simply to + reduce the number of recorded images if saving them all is not needed. + Ignored if ``save_images`` is :obj:`False`. + 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' + + They correspond to the modules :mod:`SimpleITK`, :mod:`cv2` (OpenCV), + :mod:`PIL` (Pillow Fork), 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. + image_generator: A callable taking two :obj:`float` as arguments and + returning an image as a :obj:`numpy.array`. **This argument is intended + for use in the examples of Crappy, to apply an artificial strain on a + base image. Most users should ignore it.** When given, the ``camera`` + argument is ignored and the images are acquired from the generator. To + apply a strain on the image, strain values (in `%`) should be sent to + the Camera Block over the labels ``'Exx(%)'`` and ``'Eyy(%)'``. + img_shape: The shape of the images returned by the + :class:`~crappy.camera.Camera` object as a :obj:`tuple` of :obj:`int`. + It should correspond to the value returned by :obj:`numpy.shape`. + **This argument is always ignored as** ``config`` **cannot be set to** + :obj:`False`. This might change in the future. + img_dtype: The `dtype` of the images returned by the + :class:`~crappy.camera.Camera` object, as a :obj:`str`. It should + correspond to a valid data type in :mod:`numpy`, e.g. ``'uint8'``. + **This argument is always ignored as** ``config`` **cannot be set to** + :obj:`False`. This might change in the future. + labels: The labels to use for sending data to downstream Blocks. If not + given, the default labels are + ``'t(s)', 'meta', 'Coord(px)', 'Eyy(%)', 'Exx(%)'``. They carry for + each image its timestamp, a :obj:`dict` containing its metadata, a + :obj:`list` containing for each spot the coordinates of its center in a + :obj:`tuple` of :obj:`int`, and the `y` and `x` strain values + calculated from the displacement and the initial position of the + spots. If different labels are desired, they should all be provided at + once in an iterable of :obj:`str` containing the correct number of + labels (5). + raise_on_lost_spot: If :obj:`True`, raises an exception when losing the + spots to track, which stops the test. Otherwise, stops the tracking but + lets the test go on and silently sleeps. + white_spots: If :obj:`True`, detects white objects over a black + background, else black objects over a white background. + update_thresh: If :obj:`True`, the grey level threshold for detecting the + spots is re-calculated at each new image. Otherwise, the first + calculated threshold is kept for the entire test. The spots are less + likely to be lost with adaptive threshold, but the measurement will be + more noisy. Adaptive threshold may also yield inconsistent results when + spots are lost. + num_spots: The number of spots to detect, as an :obj:`int` between `1` + and `4`. If given, will try to detect exactly that number of spots and + will fail if not enough spots can be detected. If left to :obj:`None`, + will detect up to `4` spots, but potentially fewer. + safe_mode: If :obj:`True`, the Block will stop and raise an exception as + soon as overlapping spots are detected. Otherwise, it will first try to + reduce the detection window to get rid of overlapping. This argument + should be used when inconsistency in the results may have critical + consequences. + border: When searching for the new position of a spot, the Block will + search in the last known bounding box of this spot plus a few + additional pixels in each direction. This argument sets the number of + additional pixels to use. It should be greater than the expected + "speed" of the spots, in pixels / frame. But if it's set too high, + noise or other spots might hinder the detection. + min_area: The minimum area an object should have to be potentially + detected as a spot. The value is given in pixels, as a surface unit. + It must of course be adapted depending on the resolution of the camera + and the size of the spots to detect. + blur: The size in pixels (as an odd :obj:`int` greater than `1`) of the + kernel to use when applying a median blur filter to the image before + the spot detection. If not given, no blurring is performed. A slight + blur improves the spot detection by smoothening the noise, but also + takes a bit more time compared to no blurring. + **kwargs: Any additional argument will be passed to the + :class:`~crappy.camera.Camera` object, and used as a kwarg to its + :meth:`~crappy.camera.Camera.open` method. + """ super().__init__(camera=camera, transform=transform, @@ -73,7 +268,7 @@ def __init__(self, else: self.labels = list(labels) - # Making sure a coherent number of labels and fields was given + # Making sure a coherent number of labels was given if len(self.labels) != 5: raise ValueError("The number of labels should be 5 !\n" "Make sure that the time label was given") @@ -90,10 +285,18 @@ def __init__(self, border=border) def prepare(self) -> None: - """""" + """This method mostly calls the :meth:`~crappy.blocks.Camera.prepare` + method of the parent class. + + In addition to that is instantiates the + :class:`~crappy.blocks.camera_processes.VideoExtensoProcess` object that + performs the video-extensometry and the tracking. + """ + # Instantiating the SpotsDetector containing the spots to track self._spot_detector = SpotsDetector(**self._detector_kw) + # Instantiating the VideoExtensoProcess self._process_proc = VideoExtensoProcess( detector=self._spot_detector, log_queue=self._log_queue, @@ -104,14 +307,24 @@ def prepare(self) -> None: super().prepare() def _configure(self) -> None: - """""" + """This method should instantiate and start the + :class:`~crappy.tool.camera_config.VideoExtensoConfig` window for + configuring the :class:`~crappy.camera.Camera` object. + + It should also handle the case when an exception is raised in the + configuration window. + """ config = None + + # Instantiating and starting the configuration window try: config = VideoExtensoConfig(self._camera, self._log_queue, self._log_level, self._spot_detector) config.main() + + # If an exception is raised in the config window, closing it before raising except (Exception,) as exc: self._logger.exception("Caught exception in the configuration window !", exc_info=exc) @@ -119,6 +332,7 @@ def _configure(self) -> None: config.stop() raise CameraConfigError + # Getting the image dtype and shape for setting the shared Array if config.shape is not None: self._img_shape = config.shape if config.dtype is not None: