cellects 0.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. cellects/__init__.py +0 -0
  2. cellects/__main__.py +49 -0
  3. cellects/config/__init__.py +0 -0
  4. cellects/config/all_vars_dict.py +155 -0
  5. cellects/core/__init__.py +0 -0
  6. cellects/core/cellects_paths.py +31 -0
  7. cellects/core/cellects_threads.py +1451 -0
  8. cellects/core/motion_analysis.py +2010 -0
  9. cellects/core/one_image_analysis.py +1061 -0
  10. cellects/core/one_video_per_blob.py +540 -0
  11. cellects/core/program_organizer.py +1316 -0
  12. cellects/core/script_based_run.py +154 -0
  13. cellects/gui/__init__.py +0 -0
  14. cellects/gui/advanced_parameters.py +1258 -0
  15. cellects/gui/cellects.py +189 -0
  16. cellects/gui/custom_widgets.py +790 -0
  17. cellects/gui/first_window.py +449 -0
  18. cellects/gui/if_several_folders_window.py +239 -0
  19. cellects/gui/image_analysis_window.py +2066 -0
  20. cellects/gui/required_output.py +232 -0
  21. cellects/gui/video_analysis_window.py +656 -0
  22. cellects/icons/__init__.py +0 -0
  23. cellects/icons/cellects_icon.icns +0 -0
  24. cellects/icons/cellects_icon.ico +0 -0
  25. cellects/image_analysis/__init__.py +0 -0
  26. cellects/image_analysis/cell_leaving_detection.py +54 -0
  27. cellects/image_analysis/cluster_flux_study.py +102 -0
  28. cellects/image_analysis/image_segmentation.py +706 -0
  29. cellects/image_analysis/morphological_operations.py +1635 -0
  30. cellects/image_analysis/network_functions.py +1757 -0
  31. cellects/image_analysis/one_image_analysis_threads.py +289 -0
  32. cellects/image_analysis/progressively_add_distant_shapes.py +508 -0
  33. cellects/image_analysis/shape_descriptors.py +1016 -0
  34. cellects/utils/__init__.py +0 -0
  35. cellects/utils/decorators.py +14 -0
  36. cellects/utils/formulas.py +637 -0
  37. cellects/utils/load_display_save.py +1054 -0
  38. cellects/utils/utilitarian.py +490 -0
  39. cellects-0.1.2.dist-info/LICENSE.odt +0 -0
  40. cellects-0.1.2.dist-info/METADATA +132 -0
  41. cellects-0.1.2.dist-info/RECORD +44 -0
  42. cellects-0.1.2.dist-info/WHEEL +5 -0
  43. cellects-0.1.2.dist-info/entry_points.txt +2 -0
  44. cellects-0.1.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1054 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ This script contains functions and classes to load, display and save various files
4
+ For example:
5
+ - PickleRick: to write and read files without conflicts
6
+ - See: Display an image using opencv
7
+ - write_video: Write a video on hard drive
8
+ """
9
+ import logging
10
+ import os
11
+ import pickle
12
+ import time
13
+ import h5py
14
+ from timeit import default_timer
15
+ import numpy as np
16
+ from numpy.typing import NDArray
17
+ import cv2
18
+ from pathlib import Path
19
+ import exifread
20
+ from exif import Image
21
+ from matplotlib import pyplot as plt
22
+ from cellects.image_analysis.image_segmentation import combine_color_spaces, get_color_spaces, generate_color_space_combination
23
+ from cellects.utils.formulas import bracket_to_uint8_image_contrast, sum_of_abs_differences
24
+ from cellects.utils.utilitarian import translate_dict
25
+
26
+
27
+ class PickleRick:
28
+ """
29
+ A class to handle safe file reading and writing operations using pickle.
30
+
31
+ This class ensures that files are not being accessed concurrently by
32
+ creating a lock file (PickleRickX.pkl) to signal that the file is open.
33
+ It includes methods to check for the lock file, write data safely,
34
+ and read data safely.
35
+ """
36
+ def __init__(self, pickle_rick_number=""):
37
+ """
38
+ Initialize a new instance of the class.
39
+
40
+ This constructor sets up initial attributes for tracking Rick's state, including
41
+ a boolean flag for waiting for Pickle Rick, a counter, the provided pickle Rick number,
42
+ and the time when the first check was performed.
43
+
44
+ Parameters
45
+ ----------
46
+ pickle_rick_number : str, optional
47
+ The number associated with Pickle Rick. Defaults to an empty string.
48
+ """
49
+ self.wait_for_pickle_rick: bool = False
50
+ self.counter = 0
51
+ self.pickle_rick_number = pickle_rick_number
52
+ self.first_check_time = default_timer()
53
+
54
+ def _check_that_file_is_not_open(self):
55
+ """
56
+ Check if a specific pickle file exists and handle it accordingly.
57
+
58
+ This function checks whether a file named `PickleRick{self.pickle_rick_number}.pkl`
59
+ exists. If the file has not been modified for more than 2 seconds, it is removed.
60
+ The function then updates an attribute to indicate whether the file exists.
61
+
62
+ Parameters
63
+ ----------
64
+ self : PickleRickObject
65
+ The instance of the class containing this method.
66
+
67
+ Returns
68
+ -------
69
+ None
70
+ This function does not return any value.
71
+ It updates the `self.wait_for_pickle_rick` attribute.
72
+
73
+ Notes
74
+ -----
75
+ This function removes the pickle file if it has not been modified for more than 2 seconds.
76
+ The `self.wait_for_pickle_rick` attribute is updated based on the existence of the file.
77
+ """
78
+ if os.path.isfile(f"PickleRick{self.pickle_rick_number}.pkl"):
79
+ if default_timer() - self.first_check_time > 2:
80
+ os.remove(f"PickleRick{self.pickle_rick_number}.pkl")
81
+ # logging.error((f"Cannot read/write, Trying again... tip: unlock by deleting the file named PickleRick{self.pickle_rick_number}.pkl"))
82
+ self.wait_for_pickle_rick = os.path.isfile(f"PickleRick{self.pickle_rick_number}.pkl")
83
+
84
+ def _write_pickle_rick(self):
85
+ """
86
+ Write pickle data to a file for Pickle Rick.
87
+
88
+ Parameters
89
+ ----------
90
+ self : object
91
+ The instance of the class that this method belongs to.
92
+ This typically contains attributes and methods relevant to managing
93
+ pickle operations for Pickle Rick.
94
+
95
+ Raises
96
+ ------
97
+ Exception
98
+ General exception raised if there is any issue with writing the file.
99
+ The error details are logged.
100
+
101
+ Notes
102
+ -----
103
+ This function creates a file named `PickleRick{self.pickle_rick_number}.pkl`
104
+ with a dictionary indicating readiness for Pickle Rick.
105
+
106
+ Examples
107
+ --------
108
+ >>> obj = PickleRick() # Assuming `YourClassInstance` is the class containing this method
109
+ >>> obj.pickle_rick_number = 1 # Set an example value for the attribute
110
+ >>> obj._write_pickle_rick() # Call the method to create and write to file
111
+ """
112
+ try:
113
+ with open(f"PickleRick{self.pickle_rick_number}.pkl", 'wb') as file_to_write:
114
+ pickle.dump({'wait_for_pickle_rick': True}, file_to_write)
115
+ except Exception as exc:
116
+ logging.error(f"Don't know how but Pickle Rick failed... Error is: {exc}")
117
+
118
+ def _delete_pickle_rick(self):
119
+ """
120
+
121
+ Delete a specific Pickle Rick file.
122
+
123
+ Deletes the pickle file associated with the current instance's
124
+ `pickle_rick_number`.
125
+
126
+ Raises
127
+ ------
128
+ FileNotFoundError
129
+ If the file with name `PickleRick{self.pickle_rick_number}.pkl` does not exist.
130
+ """
131
+ if os.path.isfile(f"PickleRick{self.pickle_rick_number}.pkl"):
132
+ os.remove(f"PickleRick{self.pickle_rick_number}.pkl")
133
+
134
+ def write_file(self, file_content, file_name):
135
+ """
136
+ Write content to a file with error handling and retry logic.
137
+
138
+ This function attempts to write the provided content into a file.
139
+ If it fails, it retries up to 100 times with some additional checks
140
+ and delays. Note that the content is serialized using pickle.
141
+
142
+ Parameters
143
+ ----------
144
+ file_content : Any
145
+ The data to be written into the file. This will be pickled.
146
+ file_name : str
147
+ The name of the file where data should be written.
148
+
149
+ Returns
150
+ -------
151
+ None
152
+
153
+ Raises
154
+ ------
155
+ Exception
156
+ If the file cannot be written after 100 attempts, an error is logged.
157
+
158
+ Notes
159
+ -----
160
+ This function uses pickle to serialize the data, which can introduce security risks
161
+ if untrusted content is being written. It performs some internal state checks,
162
+ such as verifying that the target file isn't open and whether it should delete
163
+ some internal state, represented by `_delete_pickle_rick`.
164
+
165
+ The function implements a retry mechanism with a backoff strategy that can include
166
+ random delays, though the example code does not specify these details explicitly.
167
+
168
+ Examples
169
+ --------
170
+ >>> result = PickleRick().write_file({'key': 'value'}, 'test.pkl')
171
+ Success to write file
172
+ """
173
+ self.counter += 1
174
+ if self.counter < 100:
175
+ if self.counter > 95:
176
+ self._delete_pickle_rick()
177
+ # time.sleep(np.random.choice(np.arange(1, os.cpu_count(), 0.5)))
178
+ self._check_that_file_is_not_open()
179
+ if self.wait_for_pickle_rick:
180
+ time.sleep(2)
181
+ self.write_file(file_content, file_name)
182
+ else:
183
+ self._write_pickle_rick()
184
+ try:
185
+ with open(file_name, 'wb') as file_to_write:
186
+ pickle.dump(file_content, file_to_write, protocol=0)
187
+ self._delete_pickle_rick()
188
+ logging.info(f"Success to write file")
189
+ except Exception as exc:
190
+ logging.error(f"The Pickle error on the file {file_name} is: {exc}")
191
+ self._delete_pickle_rick()
192
+ self.write_file(file_content, file_name)
193
+ else:
194
+ logging.error(f"Failed to write {file_name}")
195
+
196
+ def read_file(self, file_name):
197
+ """
198
+ Reads the contents of a file using pickle and returns it.
199
+
200
+ Parameters
201
+ ----------
202
+ file_name : str
203
+ The name of the file to be read.
204
+
205
+ Returns
206
+ -------
207
+ Union[Any, None]
208
+ The content of the file if successfully read; otherwise, `None`.
209
+
210
+ Raises
211
+ ------
212
+ Exception
213
+ If there is an error reading the file.
214
+
215
+ Notes
216
+ -----
217
+ This function attempts to read a file multiple times if it fails.
218
+ If the number of attempts exceeds 1000, it logs an error and returns `None`.
219
+
220
+ Examples
221
+ --------
222
+ >>> PickleRick().read_file("example.pkl")
223
+ Some content
224
+
225
+ >>> read_file("non_existent_file.pkl")
226
+ None
227
+ """
228
+ self.counter += 1
229
+ if self.counter < 1000:
230
+ if self.counter > 950:
231
+ self._delete_pickle_rick()
232
+ self._check_that_file_is_not_open()
233
+ if self.wait_for_pickle_rick:
234
+ time.sleep(2)
235
+ self.read_file(file_name)
236
+ else:
237
+ self._write_pickle_rick()
238
+ try:
239
+ with open(file_name, 'rb') as fileopen:
240
+ file_content = pickle.load(fileopen)
241
+ except Exception as exc:
242
+ logging.error(f"The Pickle error on the file {file_name} is: {exc}")
243
+ file_content = None
244
+ self._delete_pickle_rick()
245
+ if file_content is None:
246
+ self.read_file(file_name)
247
+ else:
248
+ logging.info(f"Success to read file")
249
+ return file_content
250
+ else:
251
+ logging.error(f"Failed to read {file_name}")
252
+
253
+
254
+ def write_video(np_array: NDArray[np.uint8], vid_name: str, is_color: bool=True, fps: int=40):
255
+ """
256
+ Write video from numpy array.
257
+
258
+ Save a numpy array as a video file. Supports .npy format for saving raw
259
+ numpy arrays and various video formats (mp4, avi, mkv) using OpenCV.
260
+ For video formats, automatically selects a suitable codec and handles
261
+ file extensions.
262
+
263
+ Parameters
264
+ ----------
265
+ np_array : ndarray of uint8
266
+ Input array containing video frames.
267
+ vid_name : str
268
+ Filename for the output video. Can include extension or not (defaults to .mp4).
269
+ is_color : bool, optional
270
+ Whether the video should be written in color. Defaults to True.
271
+ fps : int, optional
272
+ Frame rate for the video in frames per second. Defaults to 40.
273
+
274
+ Examples
275
+ --------
276
+ >>> video_array = np.random.randint(0, 255, size=(10, 100, 100, 3), dtype=np.uint8)
277
+ >>> write_video(video_array, 'output.mp4', True, 30)
278
+ Saves `video_array` as a color video 'output.mp4' with FPS 30.
279
+ >>> video_array = np.random.randint(0, 255, size=(10, 100, 100), dtype=np.uint8)
280
+ >>> write_video(video_array, 'raw_data.npy')
281
+ Saves `video_array` as a raw numpy array file without frame rate.
282
+ """
283
+ #h265 ou h265 (mp4)
284
+ # linux: fourcc = 0x00000021 -> don't forget to change it bellow as well
285
+ if vid_name[-4:] == '.npy':
286
+ with open(vid_name, 'wb') as file:
287
+ np.save(file, np_array)
288
+ else:
289
+ valid_extensions = ['.mp4', '.avi', '.mkv']
290
+ vid_ext = vid_name[-4:]
291
+ if vid_ext not in valid_extensions:
292
+ vid_name = vid_name[:-4]
293
+ vid_name += '.mp4'
294
+ vid_ext = '.mp4'
295
+ if vid_ext =='.mp4':
296
+ fourcc = 0x7634706d# VideoWriter_fourcc(*'FMP4') #(*'MP4V') (*'h265') (*'x264') (*'DIVX')
297
+ else:
298
+ fourcc = cv2.VideoWriter_fourcc('F', 'F', 'V', '1') # lossless
299
+ size = np_array.shape[2], np_array.shape[1]
300
+ vid = cv2.VideoWriter(vid_name, fourcc, float(fps), tuple(size), is_color)
301
+ for image_i in np.arange(np_array.shape[0]):
302
+ image = np_array[image_i, ...]
303
+ vid.write(image)
304
+ vid.release()
305
+
306
+
307
+ def video2numpy(vid_name: str, conversion_dict=None, background=None, true_frame_width=None):
308
+ """
309
+ Convert a video file to a NumPy array.
310
+
311
+ This function reads a video file and converts it into a NumPy array.
312
+ If a conversion dictionary is provided, the function also generates
313
+ a converted version of the video using the specified color space conversions.
314
+ If true_frame_width is provided, and it matches half of the actual frame width,
315
+ the function adjusts the frame width accordingly.
316
+
317
+ Parameters
318
+ ----------
319
+ vid_name : str
320
+ Path to the video file or .npy file containing the video data.
321
+ conversion_dict : dict, optional
322
+ Dictionary specifying color space conversions. Default is None.
323
+ background : bool, optional
324
+ Whether to subtract the background from the video frames. Default is None.
325
+ true_frame_width : int, optional
326
+ The true width of the video frames. Default is None.
327
+
328
+ Other Parameters
329
+ ----------------
330
+ background : bool, optional
331
+ Whether to subtract the background from the video frames. Default is None.
332
+ true_frame_width : int, optional
333
+ The true width of the video frames. Default is None.
334
+
335
+ Returns
336
+ -------
337
+ video : numpy.ndarray or tuple(numpy.ndarray, numpy.ndarray)
338
+ If conversion_dict is None, returns the video as a NumPy array.
339
+ Otherwise, returns a tuple containing the original and converted videos.
340
+
341
+ Raises
342
+ ------
343
+ ValueError
344
+ If the video file cannot be opened or if there is an error in processing.
345
+
346
+ Notes
347
+ -----
348
+ - This function uses OpenCV to read video files.
349
+ - If true_frame_width is provided and it matches half of the actual frame width,
350
+ the function adjusts the frame width accordingly.
351
+ - The conversion dictionary should contain color space mappings for transformation.
352
+
353
+ Examples
354
+ --------
355
+ >>> vid_array = video2numpy('example_video.mp4')
356
+ >>> print(vid_array.shape)
357
+ (100, 720, 1280, 3)
358
+
359
+ >>> vid_array, converted_vid = video2numpy('example_video.mp4', {'rgb': 'gray'}, True)
360
+ >>> print(vid_array.shape, converted_vid.shape)
361
+ (100, 720, 1280, 3) (100, 720, 640)
362
+
363
+ >>> vid_array = video2numpy('example_video.npy')
364
+ >>> print(vid_array.shape)
365
+ (100, 720, 1920, 3)
366
+
367
+ >>> vid_array = video2numpy('example_video.npy', true_frame_width=1920)
368
+ >>> print(vid_array.shape)
369
+ (100, 720, 960, 3)
370
+
371
+ >>> vid_array = video2numpy('example_video.npy', {'rgb': 'gray'}, True, 960)
372
+ >>> print(vid_array.shape)
373
+ (100, 720, 960)"""
374
+ if vid_name[-4:] == ".npy":
375
+ video = np.load(vid_name) # , allow_pickle='TRUE'
376
+ frame_width = video.shape[2]
377
+ if true_frame_width is not None:
378
+ if frame_width == 2 * true_frame_width:
379
+ frame_width = true_frame_width
380
+ if conversion_dict is not None:
381
+ converted_video = np.zeros((video.shape[0], video.shape[1], frame_width), dtype=np.uint8)
382
+ for counter in np.arange(video.shape[0]):
383
+ img = video[counter, :, :frame_width, :]
384
+ greyscale_image, greyscale_image2 = generate_color_space_combination(img, list(conversion_dict.keys()),
385
+ conversion_dict, background=background,
386
+ convert_to_uint8=True)
387
+ converted_video[counter, ...] = greyscale_image
388
+ video = video[:, :, :frame_width, ...]
389
+ else:
390
+
391
+ cap = cv2.VideoCapture(vid_name)
392
+ frame_number = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
393
+ frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
394
+ frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
395
+ if true_frame_width is not None:
396
+ if frame_width == 2 * true_frame_width:
397
+ frame_width = true_frame_width
398
+
399
+ # 2) Create empty arrays to store video analysis data
400
+
401
+ video = np.empty((frame_number, frame_height, frame_width, 3), dtype=np.uint8)
402
+ if conversion_dict is not None:
403
+ converted_video = np.empty((frame_number, frame_height, frame_width), dtype=np.uint8)
404
+ # 3) Read and convert the video frame by frame
405
+ counter = 0
406
+ while cap.isOpened() and counter < frame_number:
407
+ ret, frame = cap.read()
408
+ frame = frame[:, :frame_width, ...]
409
+ video[counter, ...] = frame
410
+ if conversion_dict is not None:
411
+ conversion_dict = translate_dict(conversion_dict)
412
+ c_spaces = get_color_spaces(frame, list(conversion_dict.keys()))
413
+ csc = combine_color_spaces(conversion_dict, c_spaces, subtract_background=background)
414
+ converted_video[counter, ...] = csc
415
+ counter += 1
416
+ cap.release()
417
+
418
+ if conversion_dict is None:
419
+ return video
420
+ else:
421
+ return video, converted_video
422
+
423
+
424
+ def movie(video, keyboard=1, increase_contrast: bool=True):
425
+ """
426
+ Summary
427
+ -------
428
+ Processes a video to display each frame with optional contrast increase and resizing.
429
+
430
+ Parameters
431
+ ----------
432
+ video : numpy.ndarray
433
+ The input video represented as a 3D NumPy array.
434
+ keyboard : int, optional
435
+ Key for waiting during display (default is 1).
436
+ increase_contrast : bool, optional
437
+ Flag to increase the contrast of each frame (default is True).
438
+
439
+ Other Parameters
440
+ ----------------
441
+ keyboard : int, optional
442
+ Key to wait for during the display of each frame.
443
+ increase_contrast : bool, optional
444
+ Whether to increase contrast for the displayed frames.
445
+
446
+ Returns
447
+ -------
448
+ None
449
+
450
+ Raises
451
+ ------
452
+ ValueError
453
+ If `video` is not a 3D NumPy array.
454
+
455
+ Notes
456
+ -----
457
+ This function uses OpenCV's `imshow` to display each frame. Ensure that the required
458
+ OpenCV dependencies are met.
459
+
460
+ Examples
461
+ --------
462
+ >>> movie(video)
463
+ Processes and displays a video with default settings.
464
+ >>> movie(video, keyboard=0)
465
+ Processes and displays a video waiting for the SPACE key between frames.
466
+ >>> movie(video, increase_contrast=False)
467
+ Processes and displays a video without increasing contrast.
468
+
469
+ """
470
+ for i in np.arange(video.shape[0]):
471
+ image = video[i, :, :]
472
+ if np.any(image):
473
+ if increase_contrast:
474
+ image = bracket_to_uint8_image_contrast(image)
475
+ final_img = cv2.resize(image, (500, 500))
476
+ cv2.imshow('Motion analysis', final_img)
477
+ cv2.waitKey(keyboard)
478
+ cv2.destroyAllWindows()
479
+
480
+
481
+ opencv_accepted_formats = [
482
+ 'bmp', 'BMP', 'dib', 'DIB', 'exr', 'EXR', 'hdr', 'HDR', 'jp2', 'JP2',
483
+ 'jpe', 'JPE', 'jpeg', 'JPEG', 'jpg', 'JPG', 'pbm', 'PBM', 'pfm', 'PFM',
484
+ 'pgm', 'PGM', 'pic', 'PIC', 'png', 'PNG', 'pnm', 'PNM', 'ppm', 'PPM',
485
+ 'ras', 'RAS', 'sr', 'SR', 'tif', 'TIF', 'tiff', 'TIFF', 'webp', 'WEBP'
486
+ ]
487
+
488
+
489
+ def is_raw_image(image_path) -> bool:
490
+ """
491
+ Determine if the image path corresponds to a raw image.
492
+
493
+ Parameters
494
+ ----------
495
+ image_path : str
496
+ The file path of the image.
497
+
498
+ Returns
499
+ -------
500
+ bool
501
+ True if the image is considered raw, False otherwise.
502
+
503
+ Examples
504
+ --------
505
+ >>> result = is_raw_image("image.jpg")
506
+ >>> print(result)
507
+ False
508
+ """
509
+ ext = image_path.split(".")[-1]
510
+ if np.isin(ext, opencv_accepted_formats):
511
+ raw_image = False
512
+ else:
513
+ raw_image = True
514
+ return raw_image
515
+
516
+
517
+ def readim(image_path, raw_image: bool=False):
518
+ """
519
+ Read an image from a file and optionally process it.
520
+
521
+ Parameters
522
+ ----------
523
+ image_path : str
524
+ Path to the image file.
525
+ raw_image : bool, optional
526
+ If True, logs an error message indicating that the raw image format cannot be processed. Default is False.
527
+
528
+ Returns
529
+ -------
530
+ ndarray
531
+ The decoded image represented as a NumPy array of shape (height, width, channels).
532
+
533
+ Raises
534
+ ------
535
+ RuntimeError
536
+ If `raw_image` is set to True, logs an error indicating that the raw image format cannot be processed.
537
+
538
+ Notes
539
+ -----
540
+ Although `raw_image` is set to False by default, currently it does not perform any raw image processing.
541
+
542
+ Examples
543
+ --------
544
+ >>> cv2.imread("example.jpg")
545
+ array([[[255, 0, 0],
546
+ [255, 0, 0]],
547
+
548
+ [[ 0, 255, 0],
549
+ [ 0, 255, 0]],
550
+
551
+ [[ 0, 0, 255],
552
+ [ 0, 0, 255]]], dtype=np.uint8)
553
+ """
554
+ if raw_image:
555
+ logging.error("Cannot read this image format. If the rawpy package can, ask for a version of Cellects using it.")
556
+ # import rawpy
557
+ # raw = rawpy.imread(image_path)
558
+ # raw = raw.postprocess()
559
+ # return cv2.cvtColor(raw, COLOR_RGB2BGR)
560
+ return cv2.imread(image_path)
561
+ else:
562
+ return cv2.imread(image_path)
563
+
564
+
565
+ def read_and_rotate(image_name, prev_img: NDArray=None, raw_images: bool=False, is_landscape: bool=True, crop_coord: NDArray=None) -> NDArray:
566
+ """
567
+ Read and rotate an image based on specified parameters.
568
+
569
+ This function reads an image from the given file name, optionally rotates
570
+ it by 90 degrees clockwise or counterclockwise based on its dimensions and
571
+ the `is_landscape` flag, and applies cropping if specified. It also compares
572
+ rotated images against a previous image to choose the best rotation.
573
+
574
+ Parameters
575
+ ----------
576
+ image_name : str
577
+ Name of the image file to read.
578
+ prev_img : ndarray, optional
579
+ Previous image for comparison. Default is `None`.
580
+ raw_images : bool, optional
581
+ Flag to read raw images. Default is `False`.
582
+ is_landscape : bool, optional
583
+ Flag to determine if the image should be considered in landscape mode.
584
+ Default is `True`.
585
+ crop_coord : ndarray, optional
586
+ Coordinates for cropping the image. Default is `None`.
587
+
588
+ Returns
589
+ -------
590
+ ndarray
591
+ Rotated and optionally cropped image.
592
+
593
+ Raises
594
+ ------
595
+ FileNotFoundError
596
+ If the specified image file does not exist.
597
+
598
+ Examples
599
+ ------
600
+ >>> pathway = Path(__name__).resolve().parents[0] / "data" / "experiment"
601
+ >>> image_name = 'image1.tif'
602
+ >>> image = read_and_rotate(pathway /image_name)
603
+ >>> print(image.shape)
604
+ (245, 300, 3)
605
+ """
606
+ if not os.path.exists(image_name):
607
+ raise FileNotFoundError(image_name)
608
+ img = readim(image_name, raw_images)
609
+ if (img.shape[0] > img.shape[1] and is_landscape) or (img.shape[0] < img.shape[1] and not is_landscape):
610
+ clockwise = cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE)
611
+ if crop_coord is not None:
612
+ clockwise = clockwise[crop_coord[0]:crop_coord[1], crop_coord[2]:crop_coord[3], ...]
613
+ if prev_img is not None:
614
+ prev_img = np.int16(prev_img)
615
+ clock_diff = sum_of_abs_differences(prev_img, np.int16(clockwise))
616
+ counter_clockwise = cv2.rotate(img, cv2.ROTATE_90_COUNTERCLOCKWISE)
617
+ if crop_coord is not None:
618
+ counter_clockwise = counter_clockwise[crop_coord[0]:crop_coord[1], crop_coord[2]:crop_coord[3], ...]
619
+ counter_clock_diff = sum_of_abs_differences(prev_img, np.int16(counter_clockwise))
620
+ if clock_diff > counter_clock_diff:
621
+ img = counter_clockwise
622
+ else:
623
+ img = clockwise
624
+ else:
625
+ img = clockwise
626
+ else:
627
+ if crop_coord is not None:
628
+ img = img[crop_coord[0]:crop_coord[1], crop_coord[2]:crop_coord[3], ...]
629
+ return img
630
+
631
+
632
+ def vstack_h5_array(file_name, table: NDArray, key: str="data"):
633
+ """
634
+ Stack tables vertically in an HDF5 file.
635
+
636
+ This function either appends the input table to an existing dataset
637
+ in the specified HDF5 file or creates a new dataset if the key doesn't exist.
638
+
639
+ Parameters
640
+ ----------
641
+ file_name : str
642
+ Path to the HDF5 file.
643
+ table : NDArray[np.uint8]
644
+ The table to be stacked vertically with the existing data.
645
+ key : str, optional
646
+ Key under which the dataset will be stored. Defaults to 'data'.
647
+
648
+ Examples
649
+ --------
650
+ >>> table = np.array([[1, 2], [3, 4]], dtype=np.uint8)
651
+ >>> vstack_h5_array('example.h5', table)
652
+ """
653
+ if os.path.exists(file_name):
654
+ # Open the file in append mode
655
+ with h5py.File(file_name, 'a') as h5f:
656
+ if key in h5f:
657
+ # Append to the existing dataset
658
+ existing_data = h5f[key][:]
659
+ new_data = np.vstack((existing_data, table))
660
+ del h5f[key]
661
+ h5f.create_dataset(key, data=new_data)
662
+ else:
663
+ # Create a new dataset if the key doesn't exist
664
+ h5f.create_dataset(key, data=table)
665
+ else:
666
+ with h5py.File(file_name, 'w') as h5f:
667
+ h5f.create_dataset(key, data=table)
668
+
669
+
670
+ def read_h5_array(file_name, key: str="data"):
671
+ """
672
+ Read data array from an HDF5 file.
673
+
674
+ This function reads a specific dataset from an HDF5 file using the provided key.
675
+
676
+ Parameters
677
+ ----------
678
+ file_name : str
679
+ The path to the HDF5 file.
680
+ key : str, optional, default: 'data'
681
+ The dataset name within the HDF5 file.
682
+
683
+ Returns
684
+ -------
685
+ ndarray
686
+ The data array from the specified dataset in the HDF5 file.
687
+ """
688
+ try:
689
+ with h5py.File(file_name, 'r') as h5f:
690
+ if key in h5f:
691
+ data = h5f[key][:]
692
+ return data
693
+ else:
694
+ raise KeyError(f"Dataset '{key}' not found in file '{file_name}'.")
695
+ except FileNotFoundError:
696
+ raise FileNotFoundError(f"The file '{file_name}' does not exist.")
697
+
698
+
699
+ def get_h5_keys(file_name):
700
+ """
701
+ Retrieve all keys from a given HDF5 file.
702
+
703
+ Parameters
704
+ ----------
705
+ file_name : str
706
+ The path to the HDF5 file from which keys are to be retrieved.
707
+
708
+ Returns
709
+ -------
710
+ list of str
711
+ A list containing all the keys present in the specified HDF5 file.
712
+
713
+ Raises
714
+ ------
715
+ FileNotFoundError
716
+ If the specified HDF5 file does not exist.
717
+ """
718
+ try:
719
+ with h5py.File(file_name, 'r') as h5f:
720
+ all_keys = list(h5f.keys())
721
+ return all_keys
722
+ except FileNotFoundError:
723
+ raise FileNotFoundError(f"The file '{file_name}' does not exist.")
724
+
725
+
726
+ def remove_h5_key(file_name, key: str="data"):
727
+ """
728
+ Remove a specified key from an HDF5 file.
729
+
730
+ This function opens an HDF5 file in append mode and deletes the specified
731
+ key if it exists. It handles exceptions related to file not found
732
+ and other runtime errors.
733
+
734
+ Parameters
735
+ ----------
736
+ file_name : str
737
+ The path to the HDF5 file from which the key should be removed.
738
+ key : str, optional
739
+ The name of the dataset or group to delete from the HDF5 file.
740
+ Default is "data".
741
+
742
+ Returns
743
+ -------
744
+ None
745
+
746
+ Raises
747
+ ------
748
+ FileNotFoundError
749
+ If the specified file does not exist.
750
+ RuntimeError
751
+ If any other error occurs during file operations.
752
+
753
+ Notes
754
+ -----
755
+ This function modifies the HDF5 file in place. Ensure you have a backup if necessary.
756
+ """
757
+ try:
758
+ with h5py.File(file_name, 'a') as h5f: # Open in append mode to modify the file
759
+ if key in h5f:
760
+ del h5f[key]
761
+ except FileNotFoundError:
762
+ raise FileNotFoundError(f"The file '{file_name}' does not exist.")
763
+ except Exception as e:
764
+ raise RuntimeError(f"An error occurred: {e}")
765
+
766
+
767
+ def get_mpl_colormap(cmap_name: str):
768
+ """
769
+ Returns a linear color range array for the given matplotlib colormap.
770
+
771
+ Parameters
772
+ ----------
773
+ cmap_name : str
774
+ The name of the colormap to get.
775
+
776
+ Returns
777
+ -------
778
+ numpy.ndarray
779
+ A 256x1x3 array of bytes representing the linear color range.
780
+
781
+ Examples
782
+ --------
783
+ >>> result = get_mpl_colormap('viridis')
784
+ >>> print(result.shape)
785
+ (256, 1, 3)
786
+
787
+ """
788
+ cmap = plt.get_cmap(cmap_name)
789
+
790
+ # Initialize the matplotlib color map
791
+ sm = plt.cm.ScalarMappable(cmap=cmap)
792
+
793
+ # Obtain linear color range
794
+ color_range = sm.to_rgba(np.linspace(0, 1, 256), bytes=True)[:, 2::-1]
795
+
796
+ return color_range.reshape(256, 1, 3)
797
+
798
+
799
+
800
+ def show(img, interactive: bool=True, cmap=None, show: bool=True):
801
+ """
802
+ Display an image using Matplotlib with optional interactivity and colormap.
803
+
804
+ Parameters
805
+ ----------
806
+ img : ndarray
807
+ The image data to be displayed.
808
+ interactive : bool, optional
809
+ If ``True``, turn on interactive mode. Default is ``True``.
810
+ cmap : str or Colormap, optional
811
+ The colormap to be used. If ``None``, the default colormap will
812
+ be used.
813
+
814
+ Other Parameters
815
+ ----------------
816
+ interactive : bool, optional
817
+ If ``True``, turn on interactive mode. Default is ``True``.
818
+ cmap : str or Colormap, optional
819
+ The colormap to be used. If ``None``, the default colormap will
820
+ be used.
821
+
822
+ Returns
823
+ -------
824
+ fig : Figure
825
+ The Matplotlib figure object containing the displayed image.
826
+ ax : AxesSubplot
827
+ The axes on which the image is plotted.
828
+
829
+ Raises
830
+ ------
831
+ ValueError
832
+ If `cmap` is not a recognized colormap name or object.
833
+
834
+ Notes
835
+ -----
836
+ If interactive mode is enabled, the user can manipulate the figure
837
+ window interactively.
838
+
839
+ Examples
840
+ --------
841
+ >>> img = np.random.rand(100, 50)
842
+ >>> fig, ax = show(img)
843
+ >>> print(fig) # doctest: +SKIP
844
+ <Figure size ... with ... Axes>
845
+
846
+ >>> fig, ax = show(img, interactive=False)
847
+ >>> print(fig) # doctest: +SKIP
848
+ <Figure size ... with ... Axes>
849
+
850
+ >>> fig, ax = show(img, cmap='gray')
851
+ >>> print(fig) # doctest: +SKIP
852
+ <Figure size ... with .... Axes>
853
+ """
854
+ if interactive:
855
+ plt.ion()
856
+ else:
857
+ plt.ioff()
858
+ sizes = img.shape[0] / 100, img.shape[1] / 100
859
+ fig = plt.figure(figsize=(sizes[1], sizes[0]))
860
+ ax = fig.gca()
861
+ if cmap is None:
862
+ ax.imshow(img, interpolation="none", extent=(0, sizes[1], 0, sizes[0]))
863
+ else:
864
+ ax.imshow(img, cmap=cmap, interpolation="none", extent=(0, sizes[1], 0, sizes[0]))
865
+
866
+ if show:
867
+ fig.tight_layout()
868
+ fig.show()
869
+
870
+ return fig, ax
871
+
872
+
873
+ def save_fig(img: NDArray, full_path, cmap=None):
874
+ """
875
+ Save an image figure to a file with specified options.
876
+
877
+ This function creates a matplotlib figure from the given image,
878
+ optionally applies a colormap, displays it briefly, saves the
879
+ figure to disk at high resolution, and closes the figure.
880
+
881
+ Parameters
882
+ ----------
883
+ img : array_like (M, N, 3)
884
+ Input image to be saved as a figure. Expected to be in RGB format.
885
+ full_path : str
886
+ The complete file path where the figure will be saved. Must include
887
+ extension (e.g., '.png', '.jpg').
888
+ cmap : str or None, optional
889
+ Colormap to be applied if the image should be displayed with a specific
890
+ color map. If `None`, no colormap is applied.
891
+
892
+ Returns
893
+ -------
894
+ None
895
+
896
+ This function does not return any value. It saves the figure to disk
897
+ at the specified location.
898
+
899
+ Raises
900
+ ------
901
+ FileNotFoundError
902
+ If the directory in `full_path` does not exist.
903
+
904
+ Examples
905
+ --------
906
+ >>> img = np.random.rand(100, 100, 3) * 255
907
+ >>> save_fig(img, 'test.png')
908
+ Creates and saves a figure from the random image to 'test.png'.
909
+
910
+ >>> save_fig(img, 'colored_test.png', cmap='viridis')
911
+ Creates and saves a figure from the random image with 'viridis' colormap
912
+ to 'colored_test.png'.
913
+ """
914
+ sizes = img.shape[0] / 100, img.shape[1] / 100
915
+ fig = plt.figure(figsize=(sizes[0], sizes[1]))
916
+ ax = fig.gca()
917
+ if cmap is None:
918
+ ax.imshow(img, interpolation="none")
919
+ else:
920
+ ax.imshow(img, cmap=cmap, interpolation="none")
921
+ plt.axis('off')
922
+ if np.min(img.shape) > 50:
923
+ fig.tight_layout()
924
+
925
+ fig.savefig(full_path, bbox_inches='tight', pad_inches=0., transparent=True, dpi=500)
926
+ plt.close(fig)
927
+
928
+
929
+ def display_boxes(binary_image: NDArray, box_diameter: int, show: bool = True):
930
+ """
931
+ Display grid lines on a binary image at specified box diameter intervals.
932
+
933
+ This function displays the given binary image with vertical and horizontal
934
+ grid lines drawn at regular intervals defined by `box_diameter`. The function
935
+ returns the total number of grid lines drawn.
936
+
937
+ Parameters
938
+ ----------
939
+ binary_image : ndarray
940
+ Binary image on which to draw the grid lines.
941
+ box_diameter : int
942
+ Diameter of each box in pixels.
943
+
944
+ Returns
945
+ -------
946
+ line_nb : int
947
+ Number of grid lines drawn, both vertical and horizontal.
948
+
949
+ Examples
950
+ --------
951
+ >>> import numpy as np
952
+ >>> binary_image = np.random.randint(0, 2, (100, 100), dtype=np.uint8)
953
+ >>> display_boxes(binary_image, box_diameter=25)
954
+ """
955
+ plt.imshow(binary_image, cmap='gray', extent=(0, binary_image.shape[1], 0, binary_image.shape[0]))
956
+ height, width = binary_image.shape
957
+ line_nb = 0
958
+ for x in range(0, width + 1, box_diameter):
959
+ line_nb += 1
960
+ plt.axvline(x=x, color='white', linewidth=1)
961
+ for y in range(0, height + 1, box_diameter):
962
+ line_nb += 1
963
+ plt.axhline(y=y, color='white', linewidth=1)
964
+
965
+ if show:
966
+ plt.show()
967
+
968
+ return line_nb
969
+
970
+
971
+ def extract_time(image_list: list, pathway="", raw_images:bool=False):
972
+ """
973
+ Extract timestamps from a list of images.
974
+
975
+ This function extracts the DateTimeOriginal or datetime values from
976
+ the EXIF data of a list of image files, and computes the total time in seconds.
977
+
978
+ Parameters
979
+ ----------
980
+ image_list : list of str
981
+ List of image file names.
982
+ pathway : str, optional
983
+ Path to the directory containing the images. Default is an empty string.
984
+ raw_images : bool, optional
985
+ If True, use the exifread library. Otherwise, use the exif library.
986
+ Default is False.
987
+
988
+ Returns
989
+ -------
990
+ time : ndarray of int64
991
+ Array containing the total time in seconds for each image.
992
+
993
+ Examples
994
+ --------
995
+ >>> pathway = Path(__name__).resolve().parents[0] / "data" / "experiment"
996
+ >>> image_list = ['image1.tif', 'image2.tif']
997
+ >>> time = extract_time(image_list, pathway)
998
+ >>> print(time)
999
+ array([0, 0])
1000
+
1001
+ Notes
1002
+ --------
1003
+ dir(my_image)
1004
+ ['<unknown EXIF tag 59932>', '<unknown EXIF tag 59933>', '_exif_ifd_pointer', '_gps_ifd_pointer', '_segments', 'aperture
1005
+ _value', 'brightness_value', 'color_space', 'components_configuration', 'compression', 'datetime', 'datetime_digitized',
1006
+ 'datetime_original', 'exif_version', 'exposure_bias_value', 'exposure_mode', 'exposure_program', 'exposure_time', 'f_
1007
+ number', 'flash', 'flashpix_version', 'focal_length', 'focal_length_in_35mm_film', 'get', 'get_file', 'get_thumbnail',
1008
+ 'gps_altitude', 'gps_altitude_ref', 'gps_datestamp', 'gps_dest_bearing', 'gps_dest_bearing_ref', 'gps_horizontal_
1009
+ positioning_error', 'gps_img_direction', 'gps_img_direction_ref', 'gps_latitude', 'gps_latitude_ref', 'gps_longitude',
1010
+ 'gps_longitude_ref', 'gps_speed', 'gps_speed_ref', 'gps_timestamp', 'has_exif', 'jpeg_interchange_format', 'jpeg_
1011
+ interchange_format_length', 'lens_make', 'lens_model', 'lens_specification', 'make', 'maker_note', 'metering_mode',
1012
+ 'model', 'orientation', 'photographic_sensitivity', 'pixel_x_dimension', 'pixel_y_dimension', 'resolution_unit',
1013
+ 'scene_capture_type', 'scene_type', 'sensing_method', 'shutter_speed_value', 'software', 'subject_area', 'subsec_time_
1014
+ digitized', 'subsec_time_original', 'white_balance', 'x_resolution', 'y_and_c_positioning', 'y_resolution']
1015
+
1016
+ """
1017
+ if isinstance(pathway, str):
1018
+ pathway = Path(pathway)
1019
+ nb = len(image_list)
1020
+ timings = np.zeros((nb, 6), dtype=np.int64)
1021
+ if raw_images:
1022
+ for i in np.arange(nb):
1023
+ with open(pathway / image_list[i], 'rb') as image_file:
1024
+ my_image = exifread.process_file(image_file, details=False, stop_tag='DateTimeOriginal')
1025
+ datetime = my_image["EXIF DateTimeOriginal"]
1026
+ datetime = datetime.values[:10] + ':' + datetime.values[11:]
1027
+ timings[i, :] = datetime.split(':')
1028
+ else:
1029
+ for i in np.arange(nb):
1030
+ with open(pathway / image_list[i], 'rb') as image_file:
1031
+ my_image = Image(image_file)
1032
+ if my_image.has_exif:
1033
+ datetime = my_image.datetime
1034
+ datetime = datetime[:10] + ':' + datetime[11:]
1035
+ timings[i, :] = datetime.split(':')
1036
+
1037
+ if np.all(timings[:, 0] == timings[0, 0]):
1038
+ if np.all(timings[:, 1] == timings[0, 1]):
1039
+ if np.all(timings[:, 2] == timings[0, 2]):
1040
+ time = timings[:, 3] * 3600 + timings[:, 4] * 60 + timings[:, 5]
1041
+ else:
1042
+ time = timings[:, 2] * 86400 + timings[:, 3] * 3600 + timings[:, 4] * 60 + timings[:, 5]
1043
+ else:
1044
+ days_per_month = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
1045
+ for j in np.arange(nb):
1046
+ month_number = timings[j, 1]#int(timings[j, 1])
1047
+ timings[j, 1] = days_per_month[month_number] * month_number
1048
+ time = (timings[:, 1] + timings[:, 2]) * 86400 + timings[:, 3] * 3600 + timings[:, 4] * 60 + timings[:, 5]
1049
+ #time = int(time)
1050
+ else:
1051
+ time = np.repeat(0, nb)#arange(1, nb * 60, 60)#"Do not experiment the 31th of december!!!"
1052
+ if time.sum() == 0:
1053
+ time = np.repeat(0, nb)#arange(1, nb * 60, 60)
1054
+ return time