simba-uw-tf-dev 4.5.8__py3-none-any.whl → 4.7.1__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 (98) hide show
  1. simba/SimBA.py +2 -2
  2. simba/assets/.recent_projects.txt +1 -0
  3. simba/assets/icons/frames_2.png +0 -0
  4. simba/assets/lookups/tooptips.json +15 -1
  5. simba/data_processors/agg_clf_counter_mp.py +52 -53
  6. simba/data_processors/blob_location_computer.py +1 -1
  7. simba/data_processors/circling_detector.py +30 -13
  8. simba/data_processors/cuda/geometry.py +45 -27
  9. simba/data_processors/cuda/image.py +1648 -1598
  10. simba/data_processors/cuda/statistics.py +72 -26
  11. simba/data_processors/cuda/timeseries.py +1 -1
  12. simba/data_processors/cue_light_analyzer.py +5 -9
  13. simba/data_processors/egocentric_aligner.py +25 -7
  14. simba/data_processors/freezing_detector.py +55 -47
  15. simba/data_processors/kleinberg_calculator.py +61 -29
  16. simba/feature_extractors/feature_subsets.py +14 -7
  17. simba/feature_extractors/mitra_feature_extractor.py +2 -2
  18. simba/feature_extractors/straub_tail_analyzer.py +4 -6
  19. simba/labelling/standard_labeller.py +1 -1
  20. simba/mixins/config_reader.py +5 -2
  21. simba/mixins/geometry_mixin.py +22 -36
  22. simba/mixins/image_mixin.py +24 -28
  23. simba/mixins/plotting_mixin.py +28 -10
  24. simba/mixins/statistics_mixin.py +48 -11
  25. simba/mixins/timeseries_features_mixin.py +1 -1
  26. simba/mixins/train_model_mixin.py +67 -29
  27. simba/model/inference_batch.py +1 -1
  28. simba/model/yolo_seg_inference.py +3 -3
  29. simba/outlier_tools/skip_outlier_correction.py +1 -1
  30. simba/plotting/ROI_feature_visualizer_mp.py +3 -5
  31. simba/plotting/clf_validator_mp.py +4 -5
  32. simba/plotting/cue_light_visualizer.py +6 -7
  33. simba/plotting/directing_animals_visualizer_mp.py +2 -3
  34. simba/plotting/distance_plotter_mp.py +378 -378
  35. simba/plotting/frame_mergerer_ffmpeg.py +137 -196
  36. simba/plotting/gantt_creator.py +29 -10
  37. simba/plotting/gantt_creator_mp.py +96 -33
  38. simba/plotting/geometry_plotter.py +270 -272
  39. simba/plotting/heat_mapper_clf_mp.py +4 -6
  40. simba/plotting/heat_mapper_location_mp.py +2 -2
  41. simba/plotting/light_dark_box_plotter.py +2 -2
  42. simba/plotting/path_plotter_mp.py +26 -29
  43. simba/plotting/plot_clf_results_mp.py +455 -454
  44. simba/plotting/pose_plotter_mp.py +28 -29
  45. simba/plotting/probability_plot_creator_mp.py +288 -288
  46. simba/plotting/roi_plotter_mp.py +31 -31
  47. simba/plotting/single_run_model_validation_video_mp.py +427 -427
  48. simba/plotting/spontaneous_alternation_plotter.py +2 -3
  49. simba/plotting/yolo_pose_track_visualizer.py +32 -27
  50. simba/plotting/yolo_pose_visualizer.py +35 -36
  51. simba/plotting/yolo_seg_visualizer.py +2 -3
  52. simba/pose_importers/simba_blob_importer.py +3 -3
  53. simba/roi_tools/roi_aggregate_stats_mp.py +5 -4
  54. simba/roi_tools/roi_clf_calculator_mp.py +4 -4
  55. simba/sandbox/analyze_runtimes.py +30 -0
  56. simba/sandbox/cuda/egocentric_rotator.py +374 -0
  57. simba/sandbox/get_cpu_pool.py +5 -0
  58. simba/sandbox/proboscis_to_tip.py +28 -0
  59. simba/sandbox/test_directionality.py +47 -0
  60. simba/sandbox/test_nonstatic_directionality.py +27 -0
  61. simba/sandbox/test_pycharm_cuda.py +51 -0
  62. simba/sandbox/test_simba_install.py +41 -0
  63. simba/sandbox/test_static_directionality.py +26 -0
  64. simba/sandbox/test_static_directionality_2d.py +26 -0
  65. simba/sandbox/verify_env.py +42 -0
  66. simba/third_party_label_appenders/transform/coco_keypoints_to_yolo.py +3 -3
  67. simba/third_party_label_appenders/transform/coco_keypoints_to_yolo_bbox.py +2 -2
  68. simba/ui/pop_ups/clf_add_remove_print_pop_up.py +37 -30
  69. simba/ui/pop_ups/clf_plot_pop_up.py +2 -2
  70. simba/ui/pop_ups/egocentric_alignment_pop_up.py +20 -21
  71. simba/ui/pop_ups/fsttc_pop_up.py +27 -25
  72. simba/ui/pop_ups/gantt_pop_up.py +31 -6
  73. simba/ui/pop_ups/interpolate_pop_up.py +2 -4
  74. simba/ui/pop_ups/kleinberg_pop_up.py +39 -40
  75. simba/ui/pop_ups/multiple_videos_to_frames_popup.py +10 -11
  76. simba/ui/pop_ups/single_video_to_frames_popup.py +10 -10
  77. simba/ui/pop_ups/video_processing_pop_up.py +186 -174
  78. simba/ui/tkinter_functions.py +10 -1
  79. simba/utils/custom_feature_extractor.py +1 -1
  80. simba/utils/data.py +90 -14
  81. simba/utils/enums.py +1 -0
  82. simba/utils/errors.py +441 -440
  83. simba/utils/lookups.py +1203 -1203
  84. simba/utils/printing.py +124 -124
  85. simba/utils/read_write.py +3769 -3721
  86. simba/utils/yolo.py +10 -1
  87. simba/video_processors/blob_tracking_executor.py +2 -2
  88. simba/video_processors/clahe_ui.py +66 -23
  89. simba/video_processors/egocentric_video_rotator.py +46 -44
  90. simba/video_processors/multi_cropper.py +1 -1
  91. simba/video_processors/video_processing.py +5264 -5300
  92. simba/video_processors/videos_to_frames.py +43 -32
  93. {simba_uw_tf_dev-4.5.8.dist-info → simba_uw_tf_dev-4.7.1.dist-info}/METADATA +4 -3
  94. {simba_uw_tf_dev-4.5.8.dist-info → simba_uw_tf_dev-4.7.1.dist-info}/RECORD +98 -86
  95. {simba_uw_tf_dev-4.5.8.dist-info → simba_uw_tf_dev-4.7.1.dist-info}/LICENSE +0 -0
  96. {simba_uw_tf_dev-4.5.8.dist-info → simba_uw_tf_dev-4.7.1.dist-info}/WHEEL +0 -0
  97. {simba_uw_tf_dev-4.5.8.dist-info → simba_uw_tf_dev-4.7.1.dist-info}/entry_points.txt +0 -0
  98. {simba_uw_tf_dev-4.5.8.dist-info → simba_uw_tf_dev-4.7.1.dist-info}/top_level.txt +0 -0
@@ -1,272 +1,270 @@
1
- import functools
2
- import multiprocessing
3
- import os
4
- from typing import List, Optional, Tuple, Union
5
-
6
- import cv2
7
- import numpy as np
8
- import pandas as pd
9
- from shapely.geometry import (LineString, MultiLineString, MultiPolygon, Point,
10
- Polygon)
11
-
12
- from simba.mixins.config_reader import ConfigReader
13
- from simba.mixins.plotting_mixin import PlottingMixin
14
- from simba.utils.checks import (check_float, check_if_dir_exists,
15
- check_if_valid_rgb_tuple, check_instance,
16
- check_int, check_iterable_length, check_str,
17
- check_valid_boolean, check_valid_lst)
18
- from simba.utils.data import create_color_palettes
19
- from simba.utils.enums import Defaults, Formats
20
- from simba.utils.errors import InvalidInputError
21
- from simba.utils.lookups import get_color_dict
22
- from simba.utils.printing import SimbaTimer, stdout_success
23
- from simba.utils.read_write import (concatenate_videos_in_folder,
24
- find_core_cnt, find_video_of_file,
25
- get_fn_ext, get_video_meta_data,
26
- remove_a_folder)
27
- from simba.utils.warnings import FrameRangeWarning
28
-
29
- ACCEPTED_TYPES = [Polygon, LineString, MultiPolygon, MultiLineString, Point]
30
- FRAME_COUNT = "frame_count"
31
-
32
- def geometry_visualizer(data: Tuple[int, pd.DataFrame],
33
- video_path: Union[str, os.PathLike],
34
- video_temp_dir: Union[str, os.PathLike],
35
- video_meta_data: dict,
36
- thickness: int,
37
- verbose: bool,
38
- intersection_clr: Optional[Tuple[int, int, int]],
39
- bg_opacity: float,
40
- colors: list,
41
- circle_size: int,
42
- shape_opacity: float):
43
-
44
- group, idx = int(data[0]), data[1].index.tolist()
45
- start_frm, end_frm = idx[0], idx[-1]
46
- fourcc = cv2.VideoWriter_fourcc(*Formats.MP4_CODEC.value)
47
- video_save_path = os.path.join(video_temp_dir, f"{group}.mp4")
48
- video_writer = cv2.VideoWriter(video_save_path, fourcc, video_meta_data["fps"], (video_meta_data["width"], video_meta_data["height"]))
49
- cap = cv2.VideoCapture(video_path)
50
- batch_shapes = data[1].values.reshape(len(data[1]), -1)
51
- for frm_cnt, frm_id in enumerate(range(start_frm, end_frm + 1)):
52
- cap.set(1, int(frm_id))
53
- ret, img = cap.read()
54
- if ret:
55
- img_cpy = img.copy()
56
- if bg_opacity != 1.0:
57
- opacity = 1 - bg_opacity
58
- h, w, clr = img.shape[:3]
59
- opacity_image = np.ones((h, w, clr), dtype=np.uint8) * int(255 * opacity)
60
- img = cv2.addWeighted(img.astype(np.uint8), 1 - opacity, opacity_image.astype(np.uint8), opacity, 0)
61
- for shape_cnt, shape in enumerate(batch_shapes[frm_cnt]):
62
- shape_clr = colors[shape_cnt]
63
- if intersection_clr is not None and shape is not None:
64
- current_shapes = batch_shapes[frm_cnt]
65
- for i in range(len(current_shapes)):
66
- if i != shape_cnt and current_shapes[i] is not None and shape.intersects(current_shapes[i]):
67
- shape_clr = intersection_clr
68
- break
69
- if isinstance(shape, Polygon):
70
- img_cpy = cv2.fillPoly(img_cpy, [np.array(shape.exterior.coords).astype(np.int32)], color=shape_clr)
71
- interior_coords = [np.array(interior.coords, dtype=np.int32).reshape((-1, 1, 2)) for interior in shape.interiors]
72
- for interior in interior_coords:
73
- img_cpy = cv2.fillPoly(img_cpy, [interior], color=(shape_clr[::-1]))
74
- elif isinstance(shape, LineString):
75
- img_cpy = cv2.fillPoly(img_cpy, [np.array(shape.coords, dtype=np.int32)], color=shape_clr)
76
- elif isinstance(shape, MultiPolygon):
77
- for polygon_cnt, polygon in enumerate(shape.geoms):
78
- img_cpy = cv2.fillPoly(img_cpy, [np.array((polygon.convex_hull.exterior.coords), dtype=np.int32)], color=shape_clr)
79
- elif isinstance(shape, MultiLineString):
80
- for line_cnt, line in enumerate(shape.geoms):
81
- img_cpy = cv2.fillPoly(img_cpy,[np.array(shape[line_cnt].coords, dtype=np.int32)], color=shape_clr)
82
- elif isinstance(shape, Point):
83
- arr = np.array((shape.coords)).astype(np.int32)
84
- x, y = arr[0][0], arr[0][1]
85
- img_cpy = cv2.circle(img_cpy,(x, y), circle_size, shape_clr, thickness)
86
- if shape_opacity is not None:
87
- img = cv2.addWeighted(img_cpy, shape_opacity, img, 1 - shape_opacity, 0, img)
88
- else:
89
- img = np.copy(img_cpy)
90
- video_writer.write(img.astype(np.uint8))
91
- if verbose:
92
- print(f"Creating frame {frm_id} / {video_meta_data['frame_count']} (CPU core: {group}, video name: {video_meta_data['video_name']})")
93
- else:
94
- FrameRangeWarning(msg=f'Frame {frm_id} in video {video_meta_data["video_name"]} could not be read.')
95
- pass
96
- video_writer.release()
97
- cap.release()
98
-
99
- return group
100
-
101
-
102
- class GeometryPlotter(ConfigReader, PlottingMixin):
103
- """
104
- A class for creating overlay geometry visualization videos based on provided geometries and video name.
105
-
106
- .. seealso::
107
- To quickly create static geometries on a white background (useful for troubleshooting unexpected geometries), see :func:`simba.mixins.geometry_mixin.GeometryMixin.view_shapes`
108
- and :func:`simba.mixins.geometry_mixin.GeometryMixin.geometry_video`
109
-
110
- .. image:: _static/img/GeometryPlotter.gif
111
- :width: 600
112
- :align: center
113
-
114
- .. video:: _static/img/GeometryPlotter_1.webm
115
- :width: 900
116
- :autoplay:
117
- :loop:
118
-
119
- .. video:: _static/img/GeometryPlotter_2.webm
120
- :width: 600
121
- :autoplay:
122
- :loop:
123
-
124
- :param List[List[Union[Polygon, LineString, MultiPolygon, MultiLineString, Point]]] geometries: List of lists of geometries for each frame. Each list contains as many entries as frames. Each list may represent a track or unique tracked object.
125
- :param Union[str, os.PathLike] video_name: Name of the input video.
126
- :param Optional[Union[str, os.PathLike]] config_path: Path to SimBA configuration file. Default: None.
127
- :param Optional[int] core_cnt: Number of CPU cores to use for parallel processing. Default: -1 (all available cores).
128
- :param Optional[Union[str, os.PathLike]] save_dir: Directory to save output videos. Default: None.
129
- :param Optional[int] thickness: Thickness of geometry outlines in pixels. Default: None.
130
- :param Optional[int] circle_size: Size of circles for Point geometries. Default: None.
131
- :param Optional[float] bg_opacity: Background video opacity (0.0-1.0). Default: 1.0.
132
- :param float shape_opacity: Shape fill opacity (0.0-1.0). Default: 0.3.
133
- :param Optional[str] palette: Color palette name for geometries. Default: None. Provide either a `palette` name, or a list of `colors`. If both are passed, `palette` is used.
134
- :param Optional[List[Union[str, Tuple[int, int, int]]]] colors: Custom colors for geometries. Default: None.
135
- :param Optional[Tuple[int, int, int]] intersection_clr: Color for geometries that intersect other geometries. Default to None (which means intersecting geometries maintain the original color while intersecting.
136
- :param Optional[bool] verbose: Print progress information. Default: True.
137
- :raises InvalidInputError: If the provided geometries contain invalid data types or if neither palette nor colors are provided.
138
- :raises CountError: If the number of shapes in the geometries does not match the number of frames in the video.
139
- """
140
-
141
- def __init__(self,
142
- geometries: List[List[Union[Polygon, LineString, MultiPolygon, MultiLineString, Point]]],
143
- video_name: Union[str, os.PathLike],
144
- config_path: Optional[Union[str, os.PathLike]] = None,
145
- core_cnt: Optional[int] = -1,
146
- save_dir: Optional[Union[str, os.PathLike]] = None,
147
- thickness: Optional[int] = None,
148
- circle_size: Optional[int] = None,
149
- intersection_clr: Optional[Tuple[int, int, int]] = None,
150
- bg_opacity: Optional[float] = 1,
151
- shape_opacity: float = 0.3,
152
- palette: Optional[str] = None,
153
- colors: Optional[List[Union[str, Tuple[int, int, int]]]] = None,
154
- verbose: Optional[bool] = True):
155
-
156
- PlottingMixin.__init__(self)
157
- check_instance(source=self.__class__.__name__, instance=geometries, accepted_types=list)
158
- check_iterable_length(source=self.__class__.__name__, val=len(geometries), min=1)
159
- if circle_size is not None:
160
- check_int(name="circle_size", value=circle_size, min_value=1)
161
- if thickness is not None:
162
- check_int(name="thickness", value=thickness, min_value=1)
163
- check_float(name="video_opacity", value=bg_opacity, min_value=0.0, max_value=1.0)
164
- check_float(name="shape_opacity", value=shape_opacity, min_value=0.0, max_value=1.0)
165
- check_valid_boolean(value=verbose, source='verbose', raise_error=True)
166
- self.color_dict = get_color_dict()
167
- check_int(name="CORE COUNT", value=core_cnt, min_value=-1, raise_error=True, unaccepted_vals=[0])
168
- self.core_cnt = core_cnt
169
- if palette is None and colors is None:
170
- raise InvalidInputError(msg='Pass palette or colors', source=self.__class__.__name__)
171
- if core_cnt == -1 or core_cnt > find_core_cnt()[0]:
172
- self.core_cnt = find_core_cnt()[0]
173
- if config_path is not None:
174
- ConfigReader.__init__(self, config_path=config_path)
175
- if os.path.isfile(video_name):
176
- self.video_path = video_name
177
- else:
178
- if config_path is None:
179
- raise InvalidInputError(msg=f'When providing a non-path video name, pass config_path')
180
- self.video_path = find_video_of_file(video_dir=self.video_dir, filename=video_name, raise_error=True)
181
- if intersection_clr is not None:
182
- check_if_valid_rgb_tuple(data=intersection_clr, raise_error=True, source=f'{self.__class__.__name__} intersection_clr')
183
- video_name = get_fn_ext(filepath=self.video_path)[1]
184
- self.video_meta_data = get_video_meta_data(video_path=self.video_path)
185
- self.shape_opacity = shape_opacity
186
- if circle_size is None:
187
- circle_size = self.get_optimal_circle_size(frame_size=(self.video_meta_data['width'], self.video_meta_data['height']), circle_frame_ratio=50)
188
- if thickness is None:
189
- thickness = circle_size
190
- if palette is None:
191
- self.colors = []
192
- check_valid_lst(data=colors, source=f'{self.__class__.__name__} colors', valid_dtypes=(str, tuple), exact_len=len(geometries))
193
- for clr in colors:
194
- if isinstance(clr, str):
195
- check_str(name=f'{self.__class__.__name__} colors', value=clr, options=list(self.color_dict.keys()))
196
- else:
197
- check_if_valid_rgb_tuple(data=clr)
198
- self.colors.append(clr)
199
- else:
200
- colors = create_color_palettes(no_animals=1, map_size=len(geometries) + 1, cmaps=[palette])
201
- self.colors = [x for xs in colors for x in xs]
202
- for i in range(len(geometries)):
203
- if len(geometries[i]) != self.video_meta_data[FRAME_COUNT]:
204
- FrameRangeWarning(msg=f"Geometry {i+1} contains {len(geometries[i])} shapes but video has {self.video_meta_data[FRAME_COUNT]} frames")
205
- self.geometries, self.video_name, self.thickness = geometries, video_name, thickness
206
- if config_path is None:
207
- check_if_dir_exists(in_dir=save_dir, source=f'{self.__class__.__name__} save_dir')
208
- self.save_dir = save_dir
209
- else:
210
- self.save_dir = os.path.join(self.frames_output_dir, "geometry_visualization")
211
- if not os.path.isdir(self.save_dir): os.makedirs(self.save_dir)
212
- self.temp_dir = os.path.join(self.save_dir, video_name, "temp")
213
- if os.path.isdir(self.temp_dir): remove_a_folder(folder_dir=self.temp_dir, ignore_errors=False)
214
- os.makedirs(self.temp_dir)
215
- self.save_path = os.path.join(self.save_dir, f'{video_name}.mp4')
216
- self.verbose, self.bg_opacity, self.palette = verbose, bg_opacity, palette
217
- self.circles_size, self.intersection_clr = circle_size, intersection_clr
218
-
219
- def run(self):
220
- video_timer = SimbaTimer(start=True)
221
- data = pd.DataFrame(self.geometries).T
222
- #data = data.head(100)
223
- data = np.array_split(data, self.core_cnt)
224
- data_splits = []
225
- for i in range(len(data)): data_splits.append((i, data[i]))
226
- del data
227
-
228
- with multiprocessing.Pool(self.core_cnt, maxtasksperchild=Defaults.LARGE_MAX_TASK_PER_CHILD.value) as pool:
229
- constants = functools.partial(geometry_visualizer,
230
- video_path=self.video_path,
231
- video_temp_dir=self.temp_dir,
232
- video_meta_data=self.video_meta_data,
233
- thickness=self.thickness,
234
- verbose=self.verbose,
235
- bg_opacity=self.bg_opacity,
236
- intersection_clr=self.intersection_clr,
237
- colors=self.colors,
238
- circle_size=self.circles_size,
239
- shape_opacity=self.shape_opacity)
240
- for cnt, result in enumerate(pool.imap(constants, data_splits, chunksize=1)):
241
- print(f"Section {result}/{len(data_splits)} complete...")
242
- pool.terminate()
243
- pool.join()
244
-
245
- print(f"Joining {self.video_name} geometry video...")
246
- concatenate_videos_in_folder(in_folder=self.temp_dir, save_path=self.save_path, remove_splits=True)
247
- video_timer.stop_timer()
248
- stdout_success(msg=f"Geometry video {self.save_path} complete!", elapsed_time=video_timer.elapsed_time_str, source=self.__class__.__name__)
249
-
250
-
251
-
252
-
253
-
254
-
255
-
256
- # if __name__ == '__main__':
257
- # from simba.utils.read_write import read_pickle
258
- # VIDEO_PATH = r"D:\ares\data\termite_2\termite.mp4"
259
- # DATA_PATH = r"D:\ares\data\termite_2\termite_2_geometries.pickle"
260
- # data = read_pickle(data_path=DATA_PATH)
261
- #
262
- # get_video_meta_data(video_path=VIDEO_PATH)
263
- #
264
- # geos = []
265
- #
266
- # for k, v in data.items():
267
- # geos.append(list(v.values()))
268
- # #geos.append([subdict[i] for subdict in data.values()])
269
- #
270
- # plotter = GeometryPlotter(geometries=geos, video_name=VIDEO_PATH, save_dir=r"D:\ares\data\termite_2\video", palette='Set1', core_cnt=32, bg_opacity=1)
271
- # plotter.run()
272
- # #max_frm = 9000
1
+ import functools
2
+ import multiprocessing
3
+ import os
4
+ from typing import List, Optional, Tuple, Union
5
+
6
+ import cv2
7
+ import numpy as np
8
+ import pandas as pd
9
+ from shapely.geometry import (LineString, MultiLineString, MultiPolygon, Point,
10
+ Polygon)
11
+
12
+ from simba.mixins.config_reader import ConfigReader
13
+ from simba.mixins.plotting_mixin import PlottingMixin
14
+ from simba.utils.checks import (check_float, check_if_dir_exists,
15
+ check_if_valid_rgb_tuple, check_instance,
16
+ check_int, check_iterable_length, check_str,
17
+ check_valid_boolean, check_valid_lst)
18
+ from simba.utils.data import create_color_palettes, terminate_cpu_pool
19
+ from simba.utils.enums import Defaults, Formats
20
+ from simba.utils.errors import InvalidInputError
21
+ from simba.utils.lookups import get_color_dict
22
+ from simba.utils.printing import SimbaTimer, stdout_success
23
+ from simba.utils.read_write import (concatenate_videos_in_folder,
24
+ find_core_cnt, find_video_of_file,
25
+ get_fn_ext, get_video_meta_data,
26
+ remove_a_folder)
27
+ from simba.utils.warnings import FrameRangeWarning
28
+
29
+ ACCEPTED_TYPES = [Polygon, LineString, MultiPolygon, MultiLineString, Point]
30
+ FRAME_COUNT = "frame_count"
31
+
32
+ def geometry_visualizer(data: Tuple[int, pd.DataFrame],
33
+ video_path: Union[str, os.PathLike],
34
+ video_temp_dir: Union[str, os.PathLike],
35
+ video_meta_data: dict,
36
+ thickness: int,
37
+ verbose: bool,
38
+ intersection_clr: Optional[Tuple[int, int, int]],
39
+ bg_opacity: float,
40
+ colors: list,
41
+ circle_size: int,
42
+ shape_opacity: float):
43
+
44
+ group, idx = int(data[0]), data[1].index.tolist()
45
+ start_frm, end_frm = idx[0], idx[-1]
46
+ fourcc = cv2.VideoWriter_fourcc(*Formats.MP4_CODEC.value)
47
+ video_save_path = os.path.join(video_temp_dir, f"{group}.mp4")
48
+ video_writer = cv2.VideoWriter(video_save_path, fourcc, video_meta_data["fps"], (video_meta_data["width"], video_meta_data["height"]))
49
+ cap = cv2.VideoCapture(video_path)
50
+ batch_shapes = data[1].values.reshape(len(data[1]), -1)
51
+ for frm_cnt, frm_id in enumerate(range(start_frm, end_frm + 1)):
52
+ cap.set(1, int(frm_id))
53
+ ret, img = cap.read()
54
+ if ret:
55
+ img_cpy = img.copy()
56
+ if bg_opacity != 1.0:
57
+ opacity = 1 - bg_opacity
58
+ h, w, clr = img.shape[:3]
59
+ opacity_image = np.ones((h, w, clr), dtype=np.uint8) * int(255 * opacity)
60
+ img = cv2.addWeighted(img.astype(np.uint8), 1 - opacity, opacity_image.astype(np.uint8), opacity, 0)
61
+ for shape_cnt, shape in enumerate(batch_shapes[frm_cnt]):
62
+ shape_clr = colors[shape_cnt]
63
+ if intersection_clr is not None and shape is not None:
64
+ current_shapes = batch_shapes[frm_cnt]
65
+ for i in range(len(current_shapes)):
66
+ if i != shape_cnt and current_shapes[i] is not None and shape.intersects(current_shapes[i]):
67
+ shape_clr = intersection_clr
68
+ break
69
+ if isinstance(shape, Polygon):
70
+ img_cpy = cv2.fillPoly(img_cpy, [np.array(shape.exterior.coords).astype(np.int32)], color=shape_clr)
71
+ interior_coords = [np.array(interior.coords, dtype=np.int32).reshape((-1, 1, 2)) for interior in shape.interiors]
72
+ for interior in interior_coords:
73
+ img_cpy = cv2.fillPoly(img_cpy, [interior], color=(shape_clr[::-1]))
74
+ elif isinstance(shape, LineString):
75
+ img_cpy = cv2.fillPoly(img_cpy, [np.array(shape.coords, dtype=np.int32)], color=shape_clr)
76
+ elif isinstance(shape, MultiPolygon):
77
+ for polygon_cnt, polygon in enumerate(shape.geoms):
78
+ img_cpy = cv2.fillPoly(img_cpy, [np.array((polygon.convex_hull.exterior.coords), dtype=np.int32)], color=shape_clr)
79
+ elif isinstance(shape, MultiLineString):
80
+ for line_cnt, line in enumerate(shape.geoms):
81
+ img_cpy = cv2.fillPoly(img_cpy,[np.array(shape[line_cnt].coords, dtype=np.int32)], color=shape_clr)
82
+ elif isinstance(shape, Point):
83
+ arr = np.array((shape.coords)).astype(np.int32)
84
+ x, y = arr[0][0], arr[0][1]
85
+ img_cpy = cv2.circle(img_cpy,(x, y), circle_size, shape_clr, thickness)
86
+ if shape_opacity is not None:
87
+ img = cv2.addWeighted(img_cpy, shape_opacity, img, 1 - shape_opacity, 0, img)
88
+ else:
89
+ img = np.copy(img_cpy)
90
+ video_writer.write(img.astype(np.uint8))
91
+ if verbose:
92
+ print(f"Creating frame {frm_id} / {video_meta_data['frame_count']} (CPU core: {group}, video name: {video_meta_data['video_name']})")
93
+ else:
94
+ FrameRangeWarning(msg=f'Frame {frm_id} in video {video_meta_data["video_name"]} could not be read.')
95
+ pass
96
+ video_writer.release()
97
+ cap.release()
98
+
99
+ return group
100
+
101
+
102
+ class GeometryPlotter(ConfigReader, PlottingMixin):
103
+ """
104
+ A class for creating overlay geometry visualization videos based on provided geometries and video name.
105
+
106
+ .. seealso::
107
+ To quickly create static geometries on a white background (useful for troubleshooting unexpected geometries), see :func:`simba.mixins.geometry_mixin.GeometryMixin.view_shapes`
108
+ and :func:`simba.mixins.geometry_mixin.GeometryMixin.geometry_video`
109
+
110
+ .. image:: _static/img/GeometryPlotter.gif
111
+ :width: 600
112
+ :align: center
113
+
114
+ .. video:: _static/img/GeometryPlotter_1.webm
115
+ :width: 900
116
+ :autoplay:
117
+ :loop:
118
+
119
+ .. video:: _static/img/GeometryPlotter_2.webm
120
+ :width: 600
121
+ :autoplay:
122
+ :loop:
123
+
124
+ :param List[List[Union[Polygon, LineString, MultiPolygon, MultiLineString, Point]]] geometries: List of lists of geometries for each frame. Each list contains as many entries as frames. Each list may represent a track or unique tracked object.
125
+ :param Union[str, os.PathLike] video_name: Name of the input video.
126
+ :param Optional[Union[str, os.PathLike]] config_path: Path to SimBA configuration file. Default: None.
127
+ :param Optional[int] core_cnt: Number of CPU cores to use for parallel processing. Default: -1 (all available cores).
128
+ :param Optional[Union[str, os.PathLike]] save_dir: Directory to save output videos. Default: None.
129
+ :param Optional[int] thickness: Thickness of geometry outlines in pixels. Default: None.
130
+ :param Optional[int] circle_size: Size of circles for Point geometries. Default: None.
131
+ :param Optional[float] bg_opacity: Background video opacity (0.0-1.0). Default: 1.0.
132
+ :param float shape_opacity: Shape fill opacity (0.0-1.0). Default: 0.3.
133
+ :param Optional[str] palette: Color palette name for geometries. Default: None. Provide either a `palette` name, or a list of `colors`. If both are passed, `palette` is used.
134
+ :param Optional[List[Union[str, Tuple[int, int, int]]]] colors: Custom colors for geometries. Default: None.
135
+ :param Optional[Tuple[int, int, int]] intersection_clr: Color for geometries that intersect other geometries. Default to None (which means intersecting geometries maintain the original color while intersecting.
136
+ :param Optional[bool] verbose: Print progress information. Default: True.
137
+ :raises InvalidInputError: If the provided geometries contain invalid data types or if neither palette nor colors are provided.
138
+ :raises CountError: If the number of shapes in the geometries does not match the number of frames in the video.
139
+ """
140
+
141
+ def __init__(self,
142
+ geometries: List[List[Union[Polygon, LineString, MultiPolygon, MultiLineString, Point]]],
143
+ video_name: Union[str, os.PathLike],
144
+ config_path: Optional[Union[str, os.PathLike]] = None,
145
+ core_cnt: Optional[int] = -1,
146
+ save_dir: Optional[Union[str, os.PathLike]] = None,
147
+ thickness: Optional[int] = None,
148
+ circle_size: Optional[int] = None,
149
+ intersection_clr: Optional[Tuple[int, int, int]] = None,
150
+ bg_opacity: Optional[float] = 1,
151
+ shape_opacity: float = 0.3,
152
+ palette: Optional[str] = None,
153
+ colors: Optional[List[Union[str, Tuple[int, int, int]]]] = None,
154
+ verbose: Optional[bool] = True):
155
+
156
+ PlottingMixin.__init__(self)
157
+ check_instance(source=self.__class__.__name__, instance=geometries, accepted_types=list)
158
+ check_iterable_length(source=self.__class__.__name__, val=len(geometries), min=1)
159
+ if circle_size is not None:
160
+ check_int(name="circle_size", value=circle_size, min_value=1)
161
+ if thickness is not None:
162
+ check_int(name="thickness", value=thickness, min_value=1)
163
+ check_float(name="video_opacity", value=bg_opacity, min_value=0.0, max_value=1.0)
164
+ check_float(name="shape_opacity", value=shape_opacity, min_value=0.0, max_value=1.0)
165
+ check_valid_boolean(value=verbose, source='verbose', raise_error=True)
166
+ self.color_dict = get_color_dict()
167
+ check_int(name="CORE COUNT", value=core_cnt, min_value=-1, raise_error=True, unaccepted_vals=[0])
168
+ self.core_cnt = core_cnt
169
+ if palette is None and colors is None:
170
+ raise InvalidInputError(msg='Pass palette or colors', source=self.__class__.__name__)
171
+ if core_cnt == -1 or core_cnt > find_core_cnt()[0]:
172
+ self.core_cnt = find_core_cnt()[0]
173
+ if config_path is not None:
174
+ ConfigReader.__init__(self, config_path=config_path)
175
+ if os.path.isfile(video_name):
176
+ self.video_path = video_name
177
+ else:
178
+ if config_path is None:
179
+ raise InvalidInputError(msg=f'When providing a non-path video name, pass config_path')
180
+ self.video_path = find_video_of_file(video_dir=self.video_dir, filename=video_name, raise_error=True)
181
+ if intersection_clr is not None:
182
+ check_if_valid_rgb_tuple(data=intersection_clr, raise_error=True, source=f'{self.__class__.__name__} intersection_clr')
183
+ video_name = get_fn_ext(filepath=self.video_path)[1]
184
+ self.video_meta_data = get_video_meta_data(video_path=self.video_path)
185
+ self.shape_opacity = shape_opacity
186
+ if circle_size is None:
187
+ circle_size = self.get_optimal_circle_size(frame_size=(self.video_meta_data['width'], self.video_meta_data['height']), circle_frame_ratio=50)
188
+ if thickness is None:
189
+ thickness = circle_size
190
+ if palette is None:
191
+ self.colors = []
192
+ check_valid_lst(data=colors, source=f'{self.__class__.__name__} colors', valid_dtypes=(str, tuple), exact_len=len(geometries))
193
+ for clr in colors:
194
+ if isinstance(clr, str):
195
+ check_str(name=f'{self.__class__.__name__} colors', value=clr, options=list(self.color_dict.keys()))
196
+ else:
197
+ check_if_valid_rgb_tuple(data=clr)
198
+ self.colors.append(clr)
199
+ else:
200
+ colors = create_color_palettes(no_animals=1, map_size=len(geometries) + 1, cmaps=[palette])
201
+ self.colors = [x for xs in colors for x in xs]
202
+ for i in range(len(geometries)):
203
+ if len(geometries[i]) != self.video_meta_data[FRAME_COUNT]:
204
+ FrameRangeWarning(msg=f"Geometry {i+1} contains {len(geometries[i])} shapes but video has {self.video_meta_data[FRAME_COUNT]} frames")
205
+ self.geometries, self.video_name, self.thickness = geometries, video_name, thickness
206
+ if config_path is None:
207
+ check_if_dir_exists(in_dir=save_dir, source=f'{self.__class__.__name__} save_dir')
208
+ self.save_dir = save_dir
209
+ else:
210
+ self.save_dir = os.path.join(self.frames_output_dir, "geometry_visualization")
211
+ if not os.path.isdir(self.save_dir): os.makedirs(self.save_dir)
212
+ self.temp_dir = os.path.join(self.save_dir, video_name, "temp")
213
+ if os.path.isdir(self.temp_dir): remove_a_folder(folder_dir=self.temp_dir, ignore_errors=False)
214
+ os.makedirs(self.temp_dir)
215
+ self.save_path = os.path.join(self.save_dir, f'{video_name}.mp4')
216
+ self.verbose, self.bg_opacity, self.palette = verbose, bg_opacity, palette
217
+ self.circles_size, self.intersection_clr = circle_size, intersection_clr
218
+
219
+ def run(self):
220
+ video_timer = SimbaTimer(start=True)
221
+ data = pd.DataFrame(self.geometries).T
222
+ data = np.array_split(data, self.core_cnt)
223
+ data_splits = []
224
+ for i in range(len(data)): data_splits.append((i, data[i]))
225
+ del data
226
+
227
+ with multiprocessing.Pool(self.core_cnt, maxtasksperchild=Defaults.LARGE_MAX_TASK_PER_CHILD.value) as pool:
228
+ constants = functools.partial(geometry_visualizer,
229
+ video_path=self.video_path,
230
+ video_temp_dir=self.temp_dir,
231
+ video_meta_data=self.video_meta_data,
232
+ thickness=self.thickness,
233
+ verbose=self.verbose,
234
+ bg_opacity=self.bg_opacity,
235
+ intersection_clr=self.intersection_clr,
236
+ colors=self.colors,
237
+ circle_size=self.circles_size,
238
+ shape_opacity=self.shape_opacity)
239
+ for cnt, result in enumerate(pool.imap(constants, data_splits, chunksize=1)):
240
+ print(f"Section {result}/{len(data_splits)} complete...")
241
+
242
+ terminate_cpu_pool(pool=pool, force=False)
243
+ print(f"Joining {self.video_name} geometry video...")
244
+ concatenate_videos_in_folder(in_folder=self.temp_dir, save_path=self.save_path, remove_splits=True)
245
+ video_timer.stop_timer()
246
+ stdout_success(msg=f"Geometry video {self.save_path} complete!", elapsed_time=video_timer.elapsed_time_str, source=self.__class__.__name__)
247
+
248
+
249
+
250
+
251
+
252
+
253
+
254
+ # if __name__ == '__main__':
255
+ # from simba.utils.read_write import read_pickle
256
+ # VIDEO_PATH = r"D:\ares\data\termite_2\termite.mp4"
257
+ # DATA_PATH = r"D:\ares\data\termite_2\termite_2_geometries.pickle"
258
+ # data = read_pickle(data_path=DATA_PATH)
259
+ #
260
+ # get_video_meta_data(video_path=VIDEO_PATH)
261
+ #
262
+ # geos = []
263
+ #
264
+ # for k, v in data.items():
265
+ # geos.append(list(v.values()))
266
+ # #geos.append([subdict[i] for subdict in data.values()])
267
+ #
268
+ # plotter = GeometryPlotter(geometries=geos, video_name=VIDEO_PATH, save_dir=r"D:\ares\data\termite_2\video", palette='Set1', core_cnt=32, bg_opacity=1)
269
+ # plotter.run()
270
+ # #max_frm = 9000
@@ -8,8 +8,6 @@ from typing import List, Union
8
8
 
9
9
  import cv2
10
10
  import numpy as np
11
- import pandas as pd
12
- from numba import jit, prange
13
11
 
14
12
  import simba.mixins.plotting_mixin
15
13
  from simba.mixins.config_reader import ConfigReader
@@ -19,6 +17,7 @@ from simba.utils.checks import (
19
17
  check_all_file_names_are_represented_in_video_log,
20
18
  check_filepaths_in_iterable_exist, check_int, check_str,
21
19
  check_valid_boolean, check_valid_dataframe, check_valid_dict)
20
+ from simba.utils.data import terminate_cpu_pool
22
21
  from simba.utils.enums import Formats
23
22
  from simba.utils.errors import InvalidInputError, NoSpecifiedOutputError
24
23
  from simba.utils.printing import SimbaTimer, stdout_success
@@ -99,14 +98,14 @@ class HeatMapperClfMultiprocess(ConfigReader, PlottingMixin):
99
98
 
100
99
 
101
100
  :example II:
102
- >>> test = HeatMapperClfMultiprocess(config_path=r"C:\troubleshooting\RAT_NOR\project_folder\project_config.ini",
101
+ >>> test = HeatMapperClfMultiprocess(config_path=r"C:/troubleshooting/RAT_NOR/project_folder/project_config.ini",
103
102
  >>> style_attr = {'palette': 'jet', 'shading': 'gouraud', 'bin_size': 50, 'max_scale': 'auto'},
104
103
  >>> final_img_setting=True,
105
104
  >>> video_setting=True,
106
105
  >>> frame_setting=True,
107
106
  >>> bodypart='Ear_left',
108
107
  >>> clf_name='straub_tail',
109
- >>> data_paths=[r"C:\troubleshooting\RAT_NOR\project_folder\csv\test\2022-06-20_NOB_DOT_4.csv"])
108
+ >>> data_paths=[r"C:/troubleshooting/RAT_NOR/project_folder/csv/test/2022-06-20_NOB_DOT_4.csv"])
110
109
  >>> test.run()
111
110
  """
112
111
 
@@ -215,8 +214,7 @@ class HeatMapperClfMultiprocess(ConfigReader, PlottingMixin):
215
214
 
216
215
  for cnt, batch in enumerate(pool.imap(constants, frm_per_core_w_batch, chunksize=self.multiprocess_chunksize)):
217
216
  print(f'Batch core {batch+1}/{self.core_cnt} complete (Video {self.video_name})... ')
218
- pool.terminate()
219
- pool.join()
217
+ terminate_cpu_pool(pool=pool, force=False)
220
218
 
221
219
  if self.video_setting:
222
220
  print(f"Joining {self.video_name} multiprocessed video...")
@@ -15,6 +15,7 @@ from simba.utils.checks import (
15
15
  check_all_file_names_are_represented_in_video_log,
16
16
  check_file_exist_and_readable, check_filepaths_in_iterable_exist,
17
17
  check_float, check_if_keys_exist_in_dict, check_int, check_valid_lst)
18
+ from simba.utils.data import terminate_cpu_pool
18
19
  from simba.utils.enums import Defaults, Formats, TagNames
19
20
  from simba.utils.errors import NoSpecifiedOutputError
20
21
  from simba.utils.printing import SimbaTimer, log_event, stdout_success
@@ -211,8 +212,7 @@ class HeatMapperLocationMultiprocess(ConfigReader, PlottingMixin):
211
212
  make_location_heatmap_plot=self.make_location_heatmap_plot)
212
213
  for cnt, result in enumerate(pool.imap(constants,frame_arrays,chunksize=self.multiprocess_chunksize)):
213
214
  print(f'Batch {result}/{self.core_cnt} complete... Video: {self.video_name} ({file_cnt+1}/{len(self.data_paths)})')
214
- pool.terminate()
215
- pool.join()
215
+ terminate_cpu_pool(pool=pool, force=False)
216
216
 
217
217
  if self.video_setting:
218
218
  print(f"Joining {self.video_name} multiprocessed heatmap location video...")
@@ -13,6 +13,7 @@ from simba.data_processors.light_dark_box_analyzer import LightDarkBoxAnalyzer
13
13
  from simba.mixins.plotting_mixin import PlottingMixin
14
14
  from simba.utils.checks import (check_float, check_if_dir_exists, check_str,
15
15
  check_video_and_data_frm_count_align)
16
+ from simba.utils.data import terminate_cpu_pool
16
17
  from simba.utils.enums import Defaults, Formats, Options, TextOptions
17
18
  from simba.utils.errors import NoDataError
18
19
  from simba.utils.printing import SimbaTimer, stdout_success
@@ -158,8 +159,7 @@ class LightDarkBoxPlotter():
158
159
  threshold=self.threshold)
159
160
  for cnt, result in enumerate(pool.imap(constants, pose_data, chunksize=1)):
160
161
  print(f"Section {result}/{len(pose_data)} complete...")
161
- pool.terminate()
162
- pool.join()
162
+ terminate_cpu_pool(pool=pool, force=False)
163
163
  concatenate_videos_in_folder(in_folder=self.temp_dir, save_path=self.video_save_path, gpu=False)
164
164
  video_timer.stop_timer()
165
165
  print(f"Video {self.video_save_path} complete (elapsed time: {video_timer.elapsed_time_str}s)...")