simba-uw-tf-dev 4.7.1__py3-none-any.whl → 4.7.3__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.

Potentially problematic release.


This version of simba-uw-tf-dev might be problematic. Click here for more details.

simba/utils/lookups.py CHANGED
@@ -1201,4 +1201,70 @@ def check_for_updates(time_out: int = 2):
1201
1201
  else:
1202
1202
  msg = (f'NEW SimBA VERSION AVAILABLE. \nYou have SimBA version {env_simba_version}. \nThe latest version is {latest_simba_version}. '
1203
1203
  f'\nYou can update using "pip install simba-uw-tf-dev --upgrade"')
1204
- stdout_information(msg=msg, source=check_for_updates.__name__)
1204
+ stdout_information(msg=msg, source=check_for_updates.__name__)
1205
+
1206
+
1207
+ def get_ext_codec_map() -> Dict[str, str]:
1208
+ """
1209
+ Get a dictionary mapping video file extensions to their recommended FFmpeg codecs.
1210
+ Automatically falls back to alternative codecs if the preferred codec is not available.
1211
+
1212
+ :return: Dictionary mapping file extensions (without leading dot) to codec names.
1213
+ :rtype: Dict[str, str]
1214
+
1215
+ :example:
1216
+ >>> codec_map = get_ext_codec_map()
1217
+ >>> codec = codec_map.get('webm', 'libx264') # Returns 'libvpx-vp9' or fallback
1218
+ """
1219
+ codecs_available = get_ffmpeg_encoders(raise_error=False)
1220
+ if not codecs_available: codecs_available = []
1221
+
1222
+ common_codecs = ['libx264', 'mpeg4', 'h264', 'mjpeg', 'libx265']
1223
+ fallback_codec = None
1224
+ for codec in common_codecs:
1225
+ if codec in codecs_available:
1226
+ fallback_codec = codec
1227
+ break
1228
+
1229
+ # If no common codec found, use first available or default to mpeg4 (most universal)
1230
+ if fallback_codec is None:
1231
+ fallback_codec = codecs_available[0] if codecs_available else 'mpeg4'
1232
+
1233
+ def get_codec(preferred: str, alternative: str = None) -> str:
1234
+ if preferred in codecs_available:
1235
+ return preferred
1236
+ alt = alternative if alternative else fallback_codec
1237
+ return alt if alt in codecs_available else preferred
1238
+
1239
+ return {
1240
+ 'webm': get_codec(preferred='libvpx-vp9', alternative='libvpx'),
1241
+ 'avi': get_codec(preferred='mpeg4', alternative='libx264'),
1242
+ 'mp4': get_codec(preferred='libx264', alternative='mpeg4'),
1243
+ 'mov': get_codec(preferred='libx264', alternative='mpeg4'),
1244
+ 'mkv': get_codec(preferred='libx264', alternative='mpeg4'),
1245
+ 'flv': get_codec(preferred='libx264', alternative='mpeg4'),
1246
+ 'm4v': get_codec(preferred='libx264', alternative='mpeg4'),
1247
+ 'h264': get_codec(preferred='libx264', alternative='h264'),
1248
+ }
1249
+
1250
+ def get_ffmpeg_codec(file_name: Union[str, os.PathLike],
1251
+ fallback: str = 'mpeg4') -> str:
1252
+ """
1253
+ Get the recommended FFmpeg codec for a video file based on its extension.
1254
+
1255
+ :param Union[str, os.PathLike] file_name: Path to video file or file extension.
1256
+ :param str fallback: Codec to return if file extension is not recognized. Default: 'mpeg4'.
1257
+ :return: Recommended FFmpeg codec name for the video file.
1258
+ :rtype: str
1259
+
1260
+ :example:
1261
+ >>> codec = get_ffmpeg_codec(file_name='video.mp4')
1262
+ >>> codec = get_ffmpeg_codec(file_name='video.webm', fallback='libx264')
1263
+ >>> codec = get_ffmpeg_codec(file_name=r'C:/videos/my_video.avi')
1264
+ """
1265
+ codec_map = get_ext_codec_map()
1266
+ _, file_name, ext = get_fn_ext(filepath=file_name)
1267
+ if ext[1:] in codec_map.keys():
1268
+ return codec_map[ext[1:]]
1269
+ else:
1270
+ return fallback
@@ -56,8 +56,8 @@ from simba.utils.lookups import (get_current_time,
56
56
  get_ffmpeg_crossfade_methods, get_fonts,
57
57
  get_named_colors, percent_to_crf_lookup,
58
58
  percent_to_qv_lk, quality_pct_to_crf,
59
- video_quality_to_preset_lookup)
60
- from simba.utils.printing import SimbaTimer, stdout_success
59
+ video_quality_to_preset_lookup, get_ffmpeg_codec)
60
+ from simba.utils.printing import SimbaTimer, stdout_success, stdout_information
61
61
  from simba.utils.read_write import (
62
62
  check_if_hhmmss_timestamp_is_valid_part_of_video,
63
63
  concatenate_videos_in_folder, create_directory,
@@ -704,12 +704,9 @@ def change_single_video_fps(file_path: Union[str, os.PathLike],
704
704
  quality = 23 if not check_int(name='quality', value=quality, min_value=0, max_value=52, raise_error=False)[0] else int(quality)
705
705
  if verbose: print(f"Converting the FPS {video_meta_data['fps']} -> {fps} for video {file_name} ...")
706
706
  if codec is None:
707
- if ext.lower() == '.webm':
708
- codec = 'libvpx-vp9'
709
- elif ext.lower() == '.avi':
710
- codec = 'mpeg4'
711
- else:
712
- codec = 'libx264'
707
+ codec = get_ffmpeg_codec(file_name=file_path)
708
+ else:
709
+ check_valid_codec(codec=codec, raise_error=True, source=change_single_video_fps.__name__)
713
710
  if os.path.isfile(save_path):
714
711
  FileExistWarning(msg=f"Overwriting existing file at {save_path}...", source=change_single_video_fps.__name__,)
715
712
  if gpu:
@@ -742,6 +739,9 @@ def change_fps_of_multiple_videos(path: Union[str, os.PathLike, List[Union[str,
742
739
  :param Optional[bool] gpu: If True, use NVIDEA GPU codecs. Default False.
743
740
  :param bool verbose: If True, prints conversion progress. Default True.
744
741
  :returns: None.
742
+
743
+ .. note::
744
+ Codec is automatically selected based on file extension: libvpx-vp9 for .webm, mpeg4 for .avi, libx264 for others.
745
745
 
746
746
  :example:
747
747
  >>> _ = change_fps_of_multiple_videos(path='project_folder/videos/Video_1.mp4', fps=15)
@@ -800,13 +800,17 @@ def change_fps_of_multiple_videos(path: Union[str, os.PathLike, List[Union[str,
800
800
  if verbose: stdout_success(msg=f"SIMBA COMPLETE: FPS of {len(video_paths)} video(s) changed to {fps}", elapsed_time=timer.elapsed_time_str, source=change_fps_of_multiple_videos.__name__,)
801
801
 
802
802
 
803
- def convert_video_powerpoint_compatible_format(file_path: Union[str, os.PathLike], gpu: Optional[bool] = False) -> None:
803
+ def convert_video_powerpoint_compatible_format(file_path: Union[str, os.PathLike],
804
+ gpu: Optional[bool] = False) -> None:
804
805
  """
805
806
  Create MS PowerPoint compatible copy of a video file.
806
807
 
807
808
  :param Union[str, os.PathLike] file_path: Path to video file.
808
809
  :param Optional[bool] gpu: If True, use NVIDEA GPU codecs. Default False.
809
810
  :returns: None. The result is stored in the same directory as the input file with the ``_powerpointready`` suffix.
811
+
812
+ .. note::
813
+ Codec is automatically selected: libx264 for CPU encoding (ignored if gpu=True).
810
814
 
811
815
  :example:
812
816
  >>> _ = convert_video_powerpoint_compatible_format(file_path='project_folder/videos/Video_1.mp4')
@@ -846,7 +850,7 @@ def convert_video_powerpoint_compatible_format(file_path: Union[str, os.PathLike
846
850
 
847
851
  def video_to_greyscale(file_path: Union[str, os.PathLike],
848
852
  gpu: Optional[bool] = False,
849
- codec: str = 'libx264',
853
+ codec: Optional[str] = None,
850
854
  verbose: bool = True,
851
855
  quality: int = 23,
852
856
  save_path: Optional[Union[str, os.PathLike]] = None) -> None:
@@ -890,6 +894,10 @@ def video_to_greyscale(file_path: Union[str, os.PathLike],
890
894
  check_if_dir_exists(in_dir=os.path.dirname(save_path))
891
895
  save_name = deepcopy(save_path)
892
896
  quality = 23 if not check_int(name='quality', value=quality, min_value=0, max_value=52, raise_error=False)[0] else int(quality)
897
+ if codec is None:
898
+ codec = get_ffmpeg_codec(file_name=file_path)
899
+ else:
900
+ check_valid_codec(codec=codec, raise_error=True, source=video_to_greyscale.__name__)
893
901
  if gpu:
894
902
  cmd = f'ffmpeg -hwaccel auto -c:v h264_cuvid -i "{file_path}" -vf "hwupload_cuda, hwdownload, format=nv12, format=gray" -c:v h264_nvenc -rc vbr -cq {quality} -c:a copy "{save_name}" -loglevel error -stats -hide_banner -y'
895
903
  else:
@@ -924,6 +932,9 @@ def batch_video_to_greyscale(path: Union[str, os.PathLike, List[Union[str, os.Pa
924
932
  :param Optional[bool] gpu: If True, use NVIDEA GPU codecs. Default False.
925
933
  :raise FFMPEGCodecGPUError: If no GPU is found and ``gpu == True``.
926
934
  :returns: None.
935
+
936
+ .. note::
937
+ Codec is automatically selected: libx264 for CPU encoding (ignored if gpu=True).
927
938
 
928
939
  :example:
929
940
  >>> _ = batch_video_to_greyscale(path='/Users/simon/Desktop/envs/simba/troubleshooting/mouse_open_field/project_folder/videos/test_2')
@@ -971,7 +982,7 @@ def superimpose_frame_count(file_path: Union[str, os.PathLike],
971
982
  font: Optional[str] = 'Arial',
972
983
  font_color: Optional[str] = 'black',
973
984
  bg_color: Optional[str] = 'white',
974
- codec: Optional[str] = 'libx264',
985
+ codec: Optional[str] = None,
975
986
  quality: Optional[int] = None,
976
987
  verbose: bool = True,
977
988
  save_path: Optional[Union[str, os.PathLike]] = None,
@@ -1037,11 +1048,13 @@ def superimpose_frame_count(file_path: Union[str, os.PathLike],
1037
1048
  print(len(file_paths), file_path)
1038
1049
  else:
1039
1050
  raise InvalidInputError(msg=f'{file_path} is not a valid file path or file directory.', source=superimpose_frame_count.__name__)
1040
-
1051
+ if codec is not None: check_valid_codec(codec=codec, raise_error=True, source=superimpose_frame_count.__name__)
1041
1052
  quality = 23 if not check_int(name='quality', value=quality, min_value=0, max_value=52, raise_error=False)[0] else int(quality)
1042
1053
  for video_cnt, file_path in enumerate(file_paths):
1043
1054
  dir, file_name, ext = get_fn_ext(filepath=file_path)
1044
- if verbose: print(f'Superimposing frame count video {video_cnt+1}/{len(file_paths)}...')
1055
+ if verbose:
1056
+ stdout_information(msg=f'Superimposing frame count video {video_cnt+1}/{len(file_paths)}...', source=change_single_video_fps.__name__)
1057
+ codec = get_ffmpeg_codec(file_name=file_path) if codec is None else codec
1045
1058
  if save_path is None:
1046
1059
  save_name = os.path.join(dir, f"{file_name}_frame_no.mp4")
1047
1060
  elif os.path.isdir(save_path):
@@ -1094,6 +1107,9 @@ def remove_beginning_of_video(file_path: Union[str, os.PathLike],
1094
1107
  :param Optional[Union[str, os.PathLike]] save_path: Optional save location for the shortened video. If None, then the new video is saved in the same directory as the input video with the ``_shortened`` suffix.
1095
1108
  :param Optional[bool] gpu: If True, use NVIDEA GPU codecs. Default False.
1096
1109
  :returns: None. If save_path is not passed, the result is stored in the same directory as the input file with the ``_shorten.mp4`` suffix.
1110
+
1111
+ .. note::
1112
+ Codec is automatically selected: libx264 for CPU encoding (ignored if gpu=True).
1097
1113
 
1098
1114
  :example:
1099
1115
  >>> _ = remove_beginning_of_video(file_path='project_folder/videos/Video_1.avi', time=10)
@@ -1133,7 +1149,7 @@ def clip_video_in_range(file_path: Union[str, os.PathLike],
1133
1149
  end_time: str,
1134
1150
  out_dir: Optional[Union[str, os.PathLike]] = None,
1135
1151
  save_path: Optional[Union[str, os.PathLike]] = None,
1136
- codec: str = 'libvpx-vp9',
1152
+ codec: Optional[str] = None,
1137
1153
  quality: int = 60,
1138
1154
  verbose: bool = True,
1139
1155
  overwrite: Optional[bool] = False,
@@ -1147,7 +1163,7 @@ def clip_video_in_range(file_path: Union[str, os.PathLike],
1147
1163
  :param str end_time: End time in HH:MM:SS format.
1148
1164
  :param Optional[Union[str, os.PathLike]] out_dir: If None, then the clip will be stored in the same dir as the input video. If directory, then the location of the output files.
1149
1165
  :param Optional[Union[str, os.PathLike]] save_path: Optional save path for the clipped video. If provided, overrides out_dir and filename generation. Default None.
1150
- :param str codec: Video codec to use for CPU encoding. Default 'libvpx-vp9'. Ignored if gpu=True.
1166
+ :param Optional[str] codec: Video codec to use for CPU encoding. If None, automatically selects based on file extension (libvpx-vp9 for .webm, mpeg4 for .avi, libx264 for others). Default None. Ignored if gpu=True.
1151
1167
  :param int quality: Video quality percentage (0-100). Higher values = higher quality. Default 60.
1152
1168
  :param bool verbose: If True, prints conversion progress. Default True.
1153
1169
  :param Optional[bool] overwrite: If True, overwrite output file if path already exists. If False, then raise FileExistError. Default False.
@@ -1176,7 +1192,10 @@ def clip_video_in_range(file_path: Union[str, os.PathLike],
1176
1192
  check_if_hhmmss_timestamp_is_valid_part_of_video(timestamp=end_time, video_path=file_path)
1177
1193
  quality = 60 if not check_int(name='quality', value=quality, min_value=0, max_value=100, raise_error=False)[0] else int(quality)
1178
1194
  quality_crf = quality_pct_to_crf(pct=quality)
1179
- codec = 'libvpx-vp9' if ext.lower() == '.webm' else codec
1195
+ if codec is None:
1196
+ codec = get_ffmpeg_codec(file_name=file_path)
1197
+ else:
1198
+ check_valid_codec(codec=codec, raise_error=True, source=clip_video_in_range.__name__)
1180
1199
  if not include_clip_time_in_filename:
1181
1200
  save_name = os.path.join(dir, file_name + "_clipped.mp4")
1182
1201
  else:
@@ -1200,7 +1219,7 @@ def downsample_video(file_path: Union[str, os.PathLike],
1200
1219
  video_height: int,
1201
1220
  video_width: int,
1202
1221
  gpu: bool = False,
1203
- codec: str = 'libx264',
1222
+ codec: Optional[str] = None,
1204
1223
  quality: int = 23,
1205
1224
  save_path: Optional[Union[str, os.PathLike]] = None,
1206
1225
  verbose: bool = True) -> None:
@@ -1240,6 +1259,10 @@ def downsample_video(file_path: Union[str, os.PathLike],
1240
1259
  if os.path.isfile(save_name):
1241
1260
  raise FileExistError("SIMBA ERROR: The outfile file already exist: {}.".format(save_name), source=downsample_video.__name__)
1242
1261
  quality = 23 if not check_int(name=f'{downsample_video.__name__} quality', value=quality, min_value=0, max_value=52, raise_error=False)[0] else int(quality)
1262
+ if codec is None:
1263
+ codec = get_ffmpeg_codec(file_name=file_path)
1264
+ else:
1265
+ check_valid_codec(codec=codec, raise_error=True, source=downsample_video.__name__)
1243
1266
  if gpu:
1244
1267
  command = f'ffmpeg -y -hwaccel auto -c:v h264_cuvid -i "{file_path}" -vf "scale=w={video_width}:h={video_height}" -c:v h264_nvenc -rc vbr -cq {quality} "{save_name}" -hide_banner -loglevel error -stats -y'
1245
1268
  #command = f'ffmpeg -y -hwaccel auto -c:v h264_cuvid -i "{file_path}" -vf "scale_cuda=w={video_width}:h={video_height}:force_original_aspect_ratio=decrease:flags=bicubic" -c:v h264_nvenc -rc vbr -cq {quality} "{save_name}" -loglevel error -stats -hide_banner -y'
@@ -1332,6 +1355,9 @@ def batch_convert_video_format(directory: Union[str, os.PathLike],
1332
1355
  :parameter str output_format: Format of the output files (e.g., mp4).
1333
1356
  :parameter Optional[bool] gpu: If True, use NVIDEA GPU codecs. Default False.
1334
1357
  :returns: None. The results are stored in the same directory as the input files.
1358
+
1359
+ .. note::
1360
+ Codec is automatically selected: libx264 for CPU encoding (ignored if gpu=True).
1335
1361
 
1336
1362
  :example:
1337
1363
  >>> _ = batch_convert_video_format(directory='project_folder/videos', input_format='avi', output_format='mp4')
@@ -1487,6 +1513,7 @@ def multi_split_video(file_path: Union[str, os.PathLike],
1487
1513
  end_times: List[str],
1488
1514
  out_dir: Optional[Union[str, os.PathLike]] = None,
1489
1515
  quality: Optional[int] = None,
1516
+ codec: Optional[str] = None,
1490
1517
  include_clip_time_in_filename: Optional[bool] = False,
1491
1518
  gpu: Optional[bool] = False) -> None:
1492
1519
  """
@@ -1499,6 +1526,7 @@ def multi_split_video(file_path: Union[str, os.PathLike],
1499
1526
  :param Optional[int] quality: Video quality (CRF value). Lower values = higher quality. Range 0-52. If None, defaults to 23. Default None.
1500
1527
  :param Optional[bool] include_clip_time_in_filename: If True, include the clip start and end in HH-MM-SS format as suffix in the filename. If False, then use integer suffic representing the count.
1501
1528
  :param Optional[bool] gpu: If True, use NVIDEA GPU codecs. Default False.
1529
+ :param Optional[str] codec: Video codec to use for CPU encoding. If None, automatically selects based on file extension (libvpx-vp9 for .webm, mpeg4 for .avi, libx264 for others). Default None. Ignored if gpu=True.
1502
1530
  :returns: None.
1503
1531
 
1504
1532
  :example:
@@ -1510,6 +1538,8 @@ def multi_split_video(file_path: Union[str, os.PathLike],
1510
1538
  check_file_exist_and_readable(file_path=file_path)
1511
1539
  dir_name, file_name, ext = get_fn_ext(filepath=file_path)
1512
1540
  quality = 23 if not check_int(name=f'{multi_split_video.__name__} quality', value=quality, min_value=0, max_value=52, raise_error=False)[0] else int(quality)
1541
+ if codec is not None: check_valid_codec(codec=codec, raise_error=True, source=multi_split_video.__name__)
1542
+ codec = get_ffmpeg_codec(file_name=file_name) if codec is None else codec
1513
1543
  if out_dir is not None:
1514
1544
  if not os.path.isdir(out_dir):
1515
1545
  os.makedirs(out_dir)
@@ -1555,7 +1585,7 @@ def multi_split_video(file_path: Union[str, os.PathLike],
1555
1585
  )
1556
1586
  command = f'ffmpeg -hwaccel cuda -hwaccel_output_format cuda -i "{file_path}" -ss {start_time} -to {end_time} -c:v h264_nvenc -rc vbr -cq {quality} "{save_path}" -hide_banner -loglevel error -stats -y'
1557
1587
  else:
1558
- command = f'ffmpeg -i "{file_path}" -ss {start_time} -to {end_time} -async 1 -crf {quality} "{save_path}" -loglevel error -stats -hide_banner -y'
1588
+ command = f'ffmpeg -i "{file_path}" -ss {start_time} -to {end_time} -c:v {codec} -async 1 -crf {quality} "{save_path}" -loglevel error -stats -hide_banner -y'
1559
1589
  clip_timer = SimbaTimer(start=True)
1560
1590
  subprocess.call(command, shell=True, stdout=subprocess.PIPE)
1561
1591
  clip_timer.stop_timer()
@@ -1623,6 +1653,7 @@ def crop_single_video(file_path: Union[str, os.PathLike],
1623
1653
  def crop_multiple_videos(directory_path: Union[str, os.PathLike],
1624
1654
  output_path: Union[str, os.PathLike],
1625
1655
  gpu: Optional[bool] = False,
1656
+ codec: Optional[str] = None,
1626
1657
  quality: int = 60) -> None:
1627
1658
  """
1628
1659
  Crop multiple videos in a folder according to crop-coordinates defined in the **first** video.
@@ -1636,6 +1667,8 @@ def crop_multiple_videos(directory_path: Union[str, os.PathLike],
1636
1667
  :param Union[str, os.PathLike] directory_path: Directory containing input videos.
1637
1668
  :param Union[str, os.PathLike] output_path: Directory where to save the cropped videos.
1638
1669
  :param Optional[bool] gpu: If True, use NVIDEA GPU codecs. Default False.
1670
+ :param Optional[str] codec: Video codec to use for CPU encoding. If None, automatically selects based on file extension (libvpx-vp9 for .webm, mpeg4 for .avi, libx264 for others). Default None. Ignored if gpu=True.
1671
+ :param int quality: Video quality percentage (1-100). Higher values = higher quality. Default 60.
1639
1672
  :returns: None. Results are stored in passed ``output_path``.
1640
1673
 
1641
1674
  :example:
@@ -1661,15 +1694,18 @@ def crop_multiple_videos(directory_path: Union[str, os.PathLike],
1661
1694
  if ((roi_selector.top_left[0] < 0) or (roi_selector.top_left[1] < 0) or (roi_selector.bottom_right[0] < 0) or (roi_selector.bottom_right[1] < 1)):
1662
1695
  raise CountError(msg=f"CROP FAILED: Cannot use negative crop coordinates. Got top_left: {roi_selector.top_left}, bottom_right: {roi_selector.bottom_right}", source=crop_multiple_videos.__name__)
1663
1696
  timer = SimbaTimer(start=True)
1697
+ if codec is not None: check_valid_codec(codec=codec, raise_error=True, source=change_single_video_fps.__name__)
1698
+
1664
1699
  for file_cnt, file_path in enumerate(video_paths):
1665
1700
  video_timer = SimbaTimer(start=True)
1666
1701
  dir_name, file_name, ext = get_fn_ext(filepath=file_path)
1702
+ video_codec = Formats.BATCH_CODEC.value if codec is None else get_ffmpeg_codec(file_name=file_path)
1667
1703
  print(f"Cropping video {file_name} ({file_cnt+1}/{len(video_paths)})...")
1668
1704
  video_meta_data = get_video_meta_data(file_path)
1669
1705
  if (roi_selector.bottom_right[0] > video_meta_data["width"]) or (roi_selector.bottom_right[1] > video_meta_data["height"]):
1670
1706
  raise InvalidInputError(msg=f'Cannot crop video {file_name} of size {video_meta_data["resolution_str"]} at location top left: {roi_selector.top_left}, bottom right: {roi_selector.bottom_right}', source=crop_multiple_videos.__name__)
1671
1707
  save_path = os.path.join(output_path, f"{file_name}_cropped.mp4")
1672
- crop_video(video_path=file_path, save_path=save_path, size=(roi_selector.width, roi_selector.height), top_left=(roi_selector.top_left[0], roi_selector.top_left[1]), gpu=gpu, verbose=False, quality=quality)
1708
+ crop_video(video_path=file_path, save_path=save_path, size=(roi_selector.width, roi_selector.height), top_left=(roi_selector.top_left[0], roi_selector.top_left[1]), gpu=gpu, verbose=False, quality=quality, codec=video_codec)
1673
1709
  video_timer.stop_timer()
1674
1710
  print(f"Video {file_name} cropped (Video {file_cnt+1}/{len(video_paths)}, elapsed time: {video_timer.elapsed_time_str})")
1675
1711
  timer.stop_timer()
@@ -1692,6 +1728,9 @@ def frames_to_movie(directory: Union[str, os.PathLike],
1692
1728
  :param out_format: Format of the output video. One of: 'mp4', 'avi', 'webm'. Default is 'mp4'.
1693
1729
  :param gpu: If True, use NVIDIA GPU codecs (if available). Default is False.
1694
1730
  :return: None. Video is saved to disk.
1731
+
1732
+ .. note::
1733
+ Codec is automatically selected based on out_format: libvpx-vp9 for 'webm', mpeg4 for 'avi', libx264 for 'mp4' (ignored if gpu=True).
1695
1734
  """
1696
1735
 
1697
1736
  import re
@@ -1776,6 +1815,9 @@ def video_concatenator(video_one_path: Union[str, os.PathLike],
1776
1815
  :param Optional[int] quality: Integer (1-100) representing output video quality. Higher = better quality + bigger size. If None, defaults to 60. Default None.
1777
1816
  :param Optional[bool] gpu: If True, use NVIDEA GPU codecs. Default False.
1778
1817
  :returns: None. The video is stored in the same directory as the ``video_one_path`` using the video names concatenated as filename.
1818
+
1819
+ .. note::
1820
+ Codec is automatically selected: libx264 for CPU encoding (ignored if gpu=True).
1779
1821
 
1780
1822
  :example:
1781
1823
  >>> video_concatenator(video_one_path='project_folder/videos/Video_1.mp4', video_two_path='project_folder/videos/Video_2.mp4', resolution=800, horizontal=True)
@@ -2400,6 +2442,7 @@ def resize_videos_by_height(video_paths: List[Union[str, os.PathLike]],
2400
2442
  gpu: Optional[bool] = False,
2401
2443
  quality: Optional[int] = None,
2402
2444
  suffix: Optional[str] = None,
2445
+ codec: Optional[str] = None,
2403
2446
  verbose: Optional[bool] = True) -> Union[None, List[Union[None, str, os.PathLike]]]:
2404
2447
  """
2405
2448
  Re-size a list of videos to a specified height while retaining their aspect ratios.
@@ -2442,10 +2485,13 @@ def resize_videos_by_height(video_paths: List[Union[str, os.PathLike]],
2442
2485
  for i in video_paths:
2443
2486
  video_heights.append(get_video_meta_data(video_path=i)["height"])
2444
2487
  height = video_heights[int(height)]
2488
+ if codec is not None: check_valid_codec(codec=codec, raise_error=True, source=resize_videos_by_height.__name__)
2489
+
2445
2490
  for cnt, video_path in enumerate(video_paths):
2446
2491
  dir_name, video_name, ext = get_fn_ext(video_path)
2492
+ video_codec = get_ffmpeg_codec(file_name=video_path) if codec is None else codec
2447
2493
  if verbose:
2448
- print(f"Resizing height video {video_name} (Video {cnt+1}/{len(video_paths)})...")
2494
+ stdout_information(msg=f"Resizing height video {video_name} (Video {cnt+1}/{len(video_paths)})...", source=resize_videos_by_height.__name__)
2449
2495
  if overwrite:
2450
2496
  dt = datetime.now().strftime("%Y%m%d%H%M%S")
2451
2497
  save_path = os.path.join(dir_name, f"{video_name}_{dt}.mp4")
@@ -2459,7 +2505,7 @@ def resize_videos_by_height(video_paths: List[Union[str, os.PathLike]],
2459
2505
  if gpu:
2460
2506
  cmd = f'ffmpeg -y -hwaccel auto -c:v h264_cuvid -i "{video_path}" -vf scale_npp=-2:{height} -c:v h264_nvenc -rc vbr -cq {quality} "{save_path}" -hide_banner -loglevel error -y'
2461
2507
  else:
2462
- cmd = f'ffmpeg -y -i "{video_path}" -vf scale=-2:{height} -crf {quality} "{save_path}" -hide_banner -loglevel error -stats -y'
2508
+ cmd = f'ffmpeg -y -i "{video_path}" -vf scale=-2:{height} -c:v {video_codec} -crf {quality} "{save_path}" -hide_banner -loglevel error -stats -y'
2463
2509
  subprocess.call(cmd, shell=True, stdout=subprocess.PIPE)
2464
2510
  if overwrite:
2465
2511
  shutil.copy(save_path, video_path)
@@ -2477,6 +2523,7 @@ def resize_videos_by_width(video_paths: List[Union[str, os.PathLike]],
2477
2523
  overwrite: Optional[bool] = False,
2478
2524
  save_dir: Optional[Union[str, os.PathLike]] = None,
2479
2525
  gpu: Optional[bool] = False,
2526
+ codec: Optional[str] = None,
2480
2527
  quality: Optional[int] = None,
2481
2528
  suffix: Optional[str] = None,
2482
2529
  verbose: Optional[bool] = True) -> Union[None, List[Union[None, str, os.PathLike]]]:
@@ -2527,12 +2574,12 @@ def resize_videos_by_width(video_paths: List[Union[str, os.PathLike]],
2527
2574
  for i in video_paths:
2528
2575
  video_widths.append(get_video_meta_data(video_path=i)["width"])
2529
2576
  width = video_widths[int(width)]
2577
+ if codec is not None: check_valid_codec(codec=codec, raise_error=True, source=resize_videos_by_width.__name__)
2530
2578
  for cnt, video_path in enumerate(video_paths):
2531
2579
  dir_name, video_name, ext = get_fn_ext(video_path)
2580
+ video_codec = get_ffmpeg_codec(file_name=video_path) if codec is None else codec
2532
2581
  if verbose:
2533
- print(
2534
- f"Resizing width video {video_name} (Video {cnt+1}/{len(video_paths)})..."
2535
- )
2582
+ stdout_information(msg=f"Resizing width video {video_name} (Video {cnt+1}/{len(video_paths)})...", source=resize_videos_by_height.__name__)
2536
2583
  if overwrite:
2537
2584
  dt = datetime.now().strftime("%Y%m%d%H%M%S")
2538
2585
  save_path = os.path.join(dir_name, f"{video_name}_{dt}.mp4")
@@ -2546,7 +2593,7 @@ def resize_videos_by_width(video_paths: List[Union[str, os.PathLike]],
2546
2593
  if gpu:
2547
2594
  cmd = f'ffmpeg -y -hwaccel auto -i "{video_path}" -vf "scale={width}:-2" -c:v h264_nvenc -rc vbr -cq {quality} "{save_path}" -hide_banner -loglevel error -stats -y'
2548
2595
  else:
2549
- cmd = f'ffmpeg -y -i "{video_path}" -vf scale={width}:-2 -crf {quality} "{save_path}" -hide_banner -loglevel error -stats -y'
2596
+ cmd = f'ffmpeg -y -i "{video_path}" -vf scale={width}:-2 -c:v {video_codec} -crf {quality} "{save_path}" -hide_banner -loglevel error -stats -y'
2550
2597
  subprocess.call(cmd, shell=True, stdout=subprocess.PIPE)
2551
2598
  if overwrite:
2552
2599
  shutil.copy(save_path, video_path)
@@ -2685,7 +2732,6 @@ def vertical_video_concatenator(video_paths: List[Union[str, os.PathLike]],
2685
2732
  :width: 300
2686
2733
  :align: center
2687
2734
 
2688
-
2689
2735
  .. seealso::
2690
2736
  :func:`simba.video_processors.video_processing.horizontal_video_concatenator`
2691
2737
 
@@ -2991,6 +3037,7 @@ def clip_videos_by_frame_ids(file_paths: List[Union[str, os.PathLike]],
2991
3037
  frm_ids: List[List[int]],
2992
3038
  save_dir: Optional[Union[str, os.PathLike]] = None,
2993
3039
  gpu: Optional[bool] = False,
3040
+ codec: Optional[str] = None,
2994
3041
  quality: Optional[int] = None):
2995
3042
 
2996
3043
  """
@@ -3033,13 +3080,14 @@ def clip_videos_by_frame_ids(file_paths: List[Union[str, os.PathLike]],
3033
3080
  video_timer = SimbaTimer(start=True)
3034
3081
  dir, video_name, ext = get_fn_ext(filepath=file_path)
3035
3082
  s_f, e_f = frm_ids[cnt][0], frm_ids[cnt][1]
3036
- print(f"Trimming {video_name} from frame {s_f} to frame {e_f} (Video {cnt+1}/{len(file_paths)})...")
3083
+ stdout_information(msg=f"Trimming {video_name} from frame {s_f} to frame {e_f} (Video {cnt+1}/{len(file_paths)})...", source=clip_videos_by_frame_ids.__name__)
3084
+ video_codec = get_ffmpeg_codec(file_name=file_path) if codec is None else codec
3037
3085
  if save_dir is not None:
3038
3086
  out_path = os.path.join(save_dir, os.path.basename(file_path))
3039
3087
  else:
3040
3088
  out_path = os.path.join(dir, f"{video_name}_{s_f}_{e_f}{ext}")
3041
3089
  if not gpu:
3042
- cmd = f'ffmpeg -i "{file_path}" -vf trim=start_frame={s_f}:end_frame={e_f} -an -crf {quality} "{out_path}" -loglevel error -stats -y'
3090
+ cmd = f'ffmpeg -i "{file_path}" -vf trim=start_frame={s_f}:end_frame={e_f} -c:v {video_codec} -an -crf {quality} "{out_path}" -loglevel error -stats -y'
3043
3091
  else:
3044
3092
  cmd = f'ffmpeg -hwaccel auto -i "{file_path}" -vf trim=start_frame={s_f}:end_frame={e_f} -c:v h264_nvenc -rc vbr -cq {quality} -an "{out_path}" -loglevel error -stats -y'
3045
3093
  subprocess.call(cmd, shell=True, stdout=subprocess.PIPE)
@@ -3309,6 +3357,7 @@ def convert_to_mov(path: Union[str, os.PathLike],
3309
3357
  def superimpose_video_progressbar(video_path: Union[str, os.PathLike],
3310
3358
  bar_height: Optional[int] = 10,
3311
3359
  color: Optional[str] = 'red',
3360
+ codec: Optional[str] = None,
3312
3361
  position: Optional[Literal['top', 'bottom']] = 'bottom',
3313
3362
  save_dir: Optional[Union[str, os.PathLike]] = None,
3314
3363
  gpu: Optional[bool] = False) -> None:
@@ -3345,28 +3394,30 @@ def superimpose_video_progressbar(video_path: Union[str, os.PathLike],
3345
3394
  video_path = find_all_videos_in_directory(directory=video_path, as_dict=True, raise_error=True)
3346
3395
  video_paths = list(video_path.values())
3347
3396
  else:
3348
- raise InvalidInputError(msg='{} is not a valid file path or directory path.', source=superimpose_video_progressbar.__name__)
3397
+ raise InvalidInputError(msg=f'{video_path} is not a valid file path or directory path.', source=superimpose_video_progressbar.__name__)
3349
3398
  if save_dir is not None:
3350
3399
  check_if_dir_exists(in_dir=save_dir)
3351
3400
  else:
3352
3401
  save_dir, _, _ = get_fn_ext(filepath=video_paths[0])
3402
+ if codec is not None: check_valid_codec(codec=codec, raise_error=True, source=superimpose_video_progressbar.__name__)
3353
3403
  for cnt, video_path in enumerate(video_paths):
3354
3404
  video_meta_data = get_video_meta_data(video_path=video_path)
3355
3405
  video_length = video_meta_data['video_length_s']
3356
3406
  width, height = video_meta_data['width'], video_meta_data['height']
3357
3407
  bar_height = int(height * (bar_height/100))
3358
3408
  _, video_name, ext = get_fn_ext(filepath=video_path)
3359
- print(f'Inserting progress bar on video {video_name}...')
3409
+ video_codec = get_ffmpeg_codec(file_name=video_path) if codec is None else codec
3410
+ stdout_information(msg=f'Inserting progress bar on video {video_name}...', source=superimpose_video_progressbar.__name__)
3360
3411
  save_path = os.path.join(save_dir, f'{video_name}_progress_bar{ext}')
3361
3412
  check_int(name=f'{superimpose_video_progressbar} height', value=bar_height, max_value=height, min_value=1)
3362
3413
  if position == 'bottom':
3363
3414
  if not gpu:
3364
- cmd = f'ffmpeg -i "{video_path}" -filter_complex "color=c={color}:s={width}x{bar_height}[bar];[0][bar]overlay=-w+(w/{video_length})*t:H-h:shortest=1" -c:a copy "{save_path}" -loglevel error -stats -hide_banner -y'
3415
+ cmd = f'ffmpeg -i "{video_path}" -filter_complex "color=c={color}:s={width}x{bar_height}[bar];[0][bar]overlay=-w+(w/{video_length})*t:H-h:shortest=1" -c:v {video_codec} -c:a copy "{save_path}" -loglevel error -stats -hide_banner -y'
3365
3416
  else:
3366
3417
  cmd = f'ffmpeg -hwaccel auto -i "{video_path}" -filter_complex "color=c={color}:s={width}x{bar_height}[bar];[0][bar]overlay=-w+(w/{video_length})*t:H-h:shortest=1" -c:v h264_nvenc -c:a copy "{save_path}" -loglevel error -stats -hide_banner -y'
3367
3418
  else:
3368
3419
  if not gpu:
3369
- cmd = f'ffmpeg -i "{video_path}" -filter_complex "color=c={color}:s={width}x{bar_height}[bar];[0][bar]overlay=-w+(w/{video_length})*t:{bar_height}-h:shortest=1" -c:a copy "{save_path}" -loglevel error -stats -hide_banner -y'
3420
+ cmd = f'ffmpeg -i "{video_path}" -filter_complex "color=c={color}:s={width}x{bar_height}[bar];[0][bar]overlay=-w+(w/{video_length})*t:{bar_height}-h:shortest=1" -c:a copy -c:v {video_codec} "{save_path}" -loglevel error -stats -hide_banner -y'
3370
3421
  else:
3371
3422
  cmd = f'ffmpeg -hwaccel auto -c:v h264_cuvid -i "{video_path}" -filter_complex "color=c={color}:s={width}x{bar_height}[bar];[0][bar]overlay=-w+(w/{video_length})*t:{bar_height}-h:shortest=1" -c:a copy "{save_path}" -loglevel error -stats -hide_banner -y'
3372
3423
  subprocess.call(cmd, shell=True, stdout=subprocess.PIPE)
@@ -3757,7 +3808,7 @@ def superimpose_elapsed_time(video_path: Union[str, os.PathLike],
3757
3808
 
3758
3809
  def reverse_videos(path: Union[str, os.PathLike],
3759
3810
  save_dir: Optional[Union[str, os.PathLike]] = None,
3760
- codec: Literal['libx265', 'libx264', 'vp9'] = 'libx265',
3811
+ codec: Optional[str] = None,
3761
3812
  quality: Optional[int] = 60,
3762
3813
  gpu: Optional[bool] = False) -> None:
3763
3814
 
@@ -3803,13 +3854,15 @@ def reverse_videos(path: Union[str, os.PathLike],
3803
3854
  os.makedirs(save_dir)
3804
3855
  else:
3805
3856
  raise InvalidInputError(msg=f'Path is not a valid file or directory path.', source=reverse_videos.__name__)
3857
+ if codec is not None: check_valid_codec(codec=codec, raise_error=True, source=reverse_videos.__name__)
3806
3858
  for file_cnt, file_path in enumerate(file_paths):
3807
3859
  _, video_name, ext = get_fn_ext(filepath=file_path)
3808
- print(f'Reversing video {video_name} (Video {file_cnt+1}/{len(file_paths)})...')
3860
+ stdout_information(msg=f'Reversing video {video_name} (Video {file_cnt+1}/{len(file_paths)})...', source=reverse_videos.__name__)
3861
+ video_codec = get_ffmpeg_codec(file_name=file_path) if codec is None else codec
3809
3862
  _ = get_video_meta_data(video_path=file_path)
3810
3863
  out_path = os.path.join(save_dir, f'{video_name}{ext}')
3811
3864
  if not gpu:
3812
- cmd = f'ffmpeg -i "{file_path}" -vf reverse -af areverse -c:v {codec} -crf {crf} "{out_path}" -loglevel error -stats -hide_banner -y'
3865
+ cmd = f'ffmpeg -i "{file_path}" -vf reverse -af areverse -c:v {video_codec} -crf {crf} "{out_path}" -loglevel error -stats -hide_banner -y'
3813
3866
  else:
3814
3867
  cmd = f'ffmpeg -hwaccel auto -i "{file_path}" -vf reverse -af areverse -c:v h264_nvenc -crf {crf} "{out_path}" -loglevel error -stats -hide_banner -y'
3815
3868
  subprocess.call(cmd, shell=True, stdout=subprocess.PIPE)
@@ -5080,7 +5133,7 @@ def change_playback_speed(video_path: Union[str, os.PathLike],
5080
5133
  quality: int = 60,
5081
5134
  gpu: bool = False,
5082
5135
  verbose: bool = True,
5083
- codec: str = 'libx264'):
5136
+ codec: Optional[str] = None):
5084
5137
  """
5085
5138
  Change the playback speed of a video file. Speed > 1.0 makes the video faster, speed < 1.0 makes it slower.
5086
5139
 
@@ -5123,6 +5176,10 @@ def change_playback_speed(video_path: Union[str, os.PathLike],
5123
5176
  else:
5124
5177
  save_path = os.path.join(dir, f'{video_name}_playback_speed_{speed}{ext}')
5125
5178
  video_pts = 1.0 / speed
5179
+ if codec is None:
5180
+ codec = get_ffmpeg_codec(file_name=video_path)
5181
+ else:
5182
+ check_valid_codec(codec=codec, raise_error=True, source=change_playback_speed.__name__)
5126
5183
  if gpu:
5127
5184
  cmd = f'ffmpeg -hwaccel auto -c:v h264_cuvid -i "{video_path}" -vf "setpts={video_pts:.6f}*PTS" -an -c:v h264_nvenc -rc vbr -cq {quality_code} "{save_path}" -hide_banner -loglevel error -stats -y'
5128
5185
  else:
@@ -5140,7 +5197,7 @@ def change_playback_speed_dir(data_dir: Union[str, os.PathLike],
5140
5197
  quality: int = 60,
5141
5198
  gpu: bool = False,
5142
5199
  verbose: bool = True,
5143
- codec: str = 'libx264'):
5200
+ codec: Optional[str] = None):
5144
5201
  """
5145
5202
  Change the playback speed of all video files in a directory. Speed > 1.0 makes videos faster, speed < 1.0 makes them slower.
5146
5203
 
@@ -5174,7 +5231,7 @@ def change_playback_speed_dir(data_dir: Union[str, os.PathLike],
5174
5231
  check_float(name=f'{change_playback_speed.__name__} speed', value=speed, min_value=0.000001, max_value=100, raise_error=True)
5175
5232
  check_int(name=f'{change_playback_speed.__name__} quality', value=quality, min_value=1, max_value=100, raise_error=True)
5176
5233
  check_if_dir_exists(in_dir=data_dir, source=f'{change_playback_speed.__name__} data_dir')
5177
- check_valid_codec(codec=codec, raise_error=True, source=change_playback_speed_dir.__name__)
5234
+ if codec is not None: check_valid_codec(codec=codec, raise_error=True, source=change_playback_speed_dir.__name__)
5178
5235
  video_dict = find_all_videos_in_directory(directory=data_dir, as_dict=True, raise_error=True, sort_alphabetically=True)
5179
5236
  total_video_cnt = len(list(video_dict.keys()))
5180
5237
  if save_dir is not None:
@@ -5186,9 +5243,10 @@ def change_playback_speed_dir(data_dir: Union[str, os.PathLike],
5186
5243
  raise DuplicationError(msg=f'The video directory and the save directory cannot be the same folder: {save_dir}', source=change_playback_speed_dir.__name__)
5187
5244
  for video_cnt, (video_name, video_path) in enumerate(video_dict.items()):
5188
5245
  _, _, ext = get_fn_ext(filepath=video_path)
5246
+ video_codec = get_ffmpeg_codec(file_name=video_path) if codec is None else codec
5189
5247
  save_path = os.path.join(save_dir, f'{video_name}{ext}')
5190
5248
  if verbose: print(f'Changing video speed for video {video_name} ({video_cnt+1}/{total_video_cnt})...')
5191
- change_playback_speed(video_path=video_path, speed=speed, save_path=save_path, quality=quality, codec=codec, verbose=False)
5249
+ change_playback_speed(video_path=video_path, speed=speed, save_path=save_path, quality=quality, codec=video_codec, verbose=False)
5192
5250
 
5193
5251
  timer.stop_timer()
5194
5252
  if verbose:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: simba-uw-tf-dev
3
- Version: 4.7.1
3
+ Version: 4.7.3
4
4
  Summary: Toolkit for computer classification and analysis of behaviors in experimental animals
5
5
  Home-page: https://github.com/sgoldenlab/simba
6
6
  Author: Simon Nilsson, Jia Jie Choong, Sophia Hwang