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.
- cellects/__init__.py +0 -0
- cellects/__main__.py +49 -0
- cellects/config/__init__.py +0 -0
- cellects/config/all_vars_dict.py +155 -0
- cellects/core/__init__.py +0 -0
- cellects/core/cellects_paths.py +31 -0
- cellects/core/cellects_threads.py +1451 -0
- cellects/core/motion_analysis.py +2010 -0
- cellects/core/one_image_analysis.py +1061 -0
- cellects/core/one_video_per_blob.py +540 -0
- cellects/core/program_organizer.py +1316 -0
- cellects/core/script_based_run.py +154 -0
- cellects/gui/__init__.py +0 -0
- cellects/gui/advanced_parameters.py +1258 -0
- cellects/gui/cellects.py +189 -0
- cellects/gui/custom_widgets.py +790 -0
- cellects/gui/first_window.py +449 -0
- cellects/gui/if_several_folders_window.py +239 -0
- cellects/gui/image_analysis_window.py +2066 -0
- cellects/gui/required_output.py +232 -0
- cellects/gui/video_analysis_window.py +656 -0
- cellects/icons/__init__.py +0 -0
- cellects/icons/cellects_icon.icns +0 -0
- cellects/icons/cellects_icon.ico +0 -0
- cellects/image_analysis/__init__.py +0 -0
- cellects/image_analysis/cell_leaving_detection.py +54 -0
- cellects/image_analysis/cluster_flux_study.py +102 -0
- cellects/image_analysis/image_segmentation.py +706 -0
- cellects/image_analysis/morphological_operations.py +1635 -0
- cellects/image_analysis/network_functions.py +1757 -0
- cellects/image_analysis/one_image_analysis_threads.py +289 -0
- cellects/image_analysis/progressively_add_distant_shapes.py +508 -0
- cellects/image_analysis/shape_descriptors.py +1016 -0
- cellects/utils/__init__.py +0 -0
- cellects/utils/decorators.py +14 -0
- cellects/utils/formulas.py +637 -0
- cellects/utils/load_display_save.py +1054 -0
- cellects/utils/utilitarian.py +490 -0
- cellects-0.1.2.dist-info/LICENSE.odt +0 -0
- cellects-0.1.2.dist-info/METADATA +132 -0
- cellects-0.1.2.dist-info/RECORD +44 -0
- cellects-0.1.2.dist-info/WHEEL +5 -0
- cellects-0.1.2.dist-info/entry_points.txt +2 -0
- 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
|