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