stouputils 1.14.0__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 (140) hide show
  1. stouputils/__init__.py +40 -0
  2. stouputils/__main__.py +86 -0
  3. stouputils/_deprecated.py +37 -0
  4. stouputils/all_doctests.py +160 -0
  5. stouputils/applications/__init__.py +22 -0
  6. stouputils/applications/automatic_docs.py +634 -0
  7. stouputils/applications/upscaler/__init__.py +39 -0
  8. stouputils/applications/upscaler/config.py +128 -0
  9. stouputils/applications/upscaler/image.py +247 -0
  10. stouputils/applications/upscaler/video.py +287 -0
  11. stouputils/archive.py +344 -0
  12. stouputils/backup.py +488 -0
  13. stouputils/collections.py +244 -0
  14. stouputils/continuous_delivery/__init__.py +27 -0
  15. stouputils/continuous_delivery/cd_utils.py +243 -0
  16. stouputils/continuous_delivery/github.py +522 -0
  17. stouputils/continuous_delivery/pypi.py +130 -0
  18. stouputils/continuous_delivery/pyproject.py +147 -0
  19. stouputils/continuous_delivery/stubs.py +86 -0
  20. stouputils/ctx.py +408 -0
  21. stouputils/data_science/config/get.py +51 -0
  22. stouputils/data_science/config/set.py +125 -0
  23. stouputils/data_science/data_processing/image/__init__.py +66 -0
  24. stouputils/data_science/data_processing/image/auto_contrast.py +79 -0
  25. stouputils/data_science/data_processing/image/axis_flip.py +58 -0
  26. stouputils/data_science/data_processing/image/bias_field_correction.py +74 -0
  27. stouputils/data_science/data_processing/image/binary_threshold.py +73 -0
  28. stouputils/data_science/data_processing/image/blur.py +59 -0
  29. stouputils/data_science/data_processing/image/brightness.py +54 -0
  30. stouputils/data_science/data_processing/image/canny.py +110 -0
  31. stouputils/data_science/data_processing/image/clahe.py +92 -0
  32. stouputils/data_science/data_processing/image/common.py +30 -0
  33. stouputils/data_science/data_processing/image/contrast.py +53 -0
  34. stouputils/data_science/data_processing/image/curvature_flow_filter.py +74 -0
  35. stouputils/data_science/data_processing/image/denoise.py +378 -0
  36. stouputils/data_science/data_processing/image/histogram_equalization.py +123 -0
  37. stouputils/data_science/data_processing/image/invert.py +64 -0
  38. stouputils/data_science/data_processing/image/laplacian.py +60 -0
  39. stouputils/data_science/data_processing/image/median_blur.py +52 -0
  40. stouputils/data_science/data_processing/image/noise.py +59 -0
  41. stouputils/data_science/data_processing/image/normalize.py +65 -0
  42. stouputils/data_science/data_processing/image/random_erase.py +66 -0
  43. stouputils/data_science/data_processing/image/resize.py +69 -0
  44. stouputils/data_science/data_processing/image/rotation.py +80 -0
  45. stouputils/data_science/data_processing/image/salt_pepper.py +68 -0
  46. stouputils/data_science/data_processing/image/sharpening.py +55 -0
  47. stouputils/data_science/data_processing/image/shearing.py +64 -0
  48. stouputils/data_science/data_processing/image/threshold.py +64 -0
  49. stouputils/data_science/data_processing/image/translation.py +71 -0
  50. stouputils/data_science/data_processing/image/zoom.py +83 -0
  51. stouputils/data_science/data_processing/image_augmentation.py +118 -0
  52. stouputils/data_science/data_processing/image_preprocess.py +183 -0
  53. stouputils/data_science/data_processing/prosthesis_detection.py +359 -0
  54. stouputils/data_science/data_processing/technique.py +481 -0
  55. stouputils/data_science/dataset/__init__.py +45 -0
  56. stouputils/data_science/dataset/dataset.py +292 -0
  57. stouputils/data_science/dataset/dataset_loader.py +135 -0
  58. stouputils/data_science/dataset/grouping_strategy.py +296 -0
  59. stouputils/data_science/dataset/image_loader.py +100 -0
  60. stouputils/data_science/dataset/xy_tuple.py +696 -0
  61. stouputils/data_science/metric_dictionnary.py +106 -0
  62. stouputils/data_science/metric_utils.py +847 -0
  63. stouputils/data_science/mlflow_utils.py +206 -0
  64. stouputils/data_science/models/abstract_model.py +149 -0
  65. stouputils/data_science/models/all.py +85 -0
  66. stouputils/data_science/models/base_keras.py +765 -0
  67. stouputils/data_science/models/keras/all.py +38 -0
  68. stouputils/data_science/models/keras/convnext.py +62 -0
  69. stouputils/data_science/models/keras/densenet.py +50 -0
  70. stouputils/data_science/models/keras/efficientnet.py +60 -0
  71. stouputils/data_science/models/keras/mobilenet.py +56 -0
  72. stouputils/data_science/models/keras/resnet.py +52 -0
  73. stouputils/data_science/models/keras/squeezenet.py +233 -0
  74. stouputils/data_science/models/keras/vgg.py +42 -0
  75. stouputils/data_science/models/keras/xception.py +38 -0
  76. stouputils/data_science/models/keras_utils/callbacks/__init__.py +20 -0
  77. stouputils/data_science/models/keras_utils/callbacks/colored_progress_bar.py +219 -0
  78. stouputils/data_science/models/keras_utils/callbacks/learning_rate_finder.py +148 -0
  79. stouputils/data_science/models/keras_utils/callbacks/model_checkpoint_v2.py +31 -0
  80. stouputils/data_science/models/keras_utils/callbacks/progressive_unfreezing.py +249 -0
  81. stouputils/data_science/models/keras_utils/callbacks/warmup_scheduler.py +66 -0
  82. stouputils/data_science/models/keras_utils/losses/__init__.py +12 -0
  83. stouputils/data_science/models/keras_utils/losses/next_generation_loss.py +56 -0
  84. stouputils/data_science/models/keras_utils/visualizations.py +416 -0
  85. stouputils/data_science/models/model_interface.py +939 -0
  86. stouputils/data_science/models/sandbox.py +116 -0
  87. stouputils/data_science/range_tuple.py +234 -0
  88. stouputils/data_science/scripts/augment_dataset.py +77 -0
  89. stouputils/data_science/scripts/exhaustive_process.py +133 -0
  90. stouputils/data_science/scripts/preprocess_dataset.py +70 -0
  91. stouputils/data_science/scripts/routine.py +168 -0
  92. stouputils/data_science/utils.py +285 -0
  93. stouputils/decorators.py +605 -0
  94. stouputils/image.py +441 -0
  95. stouputils/installer/__init__.py +18 -0
  96. stouputils/installer/common.py +67 -0
  97. stouputils/installer/downloader.py +101 -0
  98. stouputils/installer/linux.py +144 -0
  99. stouputils/installer/main.py +223 -0
  100. stouputils/installer/windows.py +136 -0
  101. stouputils/io.py +486 -0
  102. stouputils/parallel.py +483 -0
  103. stouputils/print.py +482 -0
  104. stouputils/py.typed +1 -0
  105. stouputils/stouputils/__init__.pyi +15 -0
  106. stouputils/stouputils/_deprecated.pyi +12 -0
  107. stouputils/stouputils/all_doctests.pyi +46 -0
  108. stouputils/stouputils/applications/__init__.pyi +2 -0
  109. stouputils/stouputils/applications/automatic_docs.pyi +106 -0
  110. stouputils/stouputils/applications/upscaler/__init__.pyi +3 -0
  111. stouputils/stouputils/applications/upscaler/config.pyi +18 -0
  112. stouputils/stouputils/applications/upscaler/image.pyi +109 -0
  113. stouputils/stouputils/applications/upscaler/video.pyi +60 -0
  114. stouputils/stouputils/archive.pyi +67 -0
  115. stouputils/stouputils/backup.pyi +109 -0
  116. stouputils/stouputils/collections.pyi +86 -0
  117. stouputils/stouputils/continuous_delivery/__init__.pyi +5 -0
  118. stouputils/stouputils/continuous_delivery/cd_utils.pyi +129 -0
  119. stouputils/stouputils/continuous_delivery/github.pyi +162 -0
  120. stouputils/stouputils/continuous_delivery/pypi.pyi +53 -0
  121. stouputils/stouputils/continuous_delivery/pyproject.pyi +67 -0
  122. stouputils/stouputils/continuous_delivery/stubs.pyi +39 -0
  123. stouputils/stouputils/ctx.pyi +211 -0
  124. stouputils/stouputils/decorators.pyi +252 -0
  125. stouputils/stouputils/image.pyi +172 -0
  126. stouputils/stouputils/installer/__init__.pyi +5 -0
  127. stouputils/stouputils/installer/common.pyi +39 -0
  128. stouputils/stouputils/installer/downloader.pyi +24 -0
  129. stouputils/stouputils/installer/linux.pyi +39 -0
  130. stouputils/stouputils/installer/main.pyi +57 -0
  131. stouputils/stouputils/installer/windows.pyi +31 -0
  132. stouputils/stouputils/io.pyi +213 -0
  133. stouputils/stouputils/parallel.pyi +216 -0
  134. stouputils/stouputils/print.pyi +136 -0
  135. stouputils/stouputils/version_pkg.pyi +15 -0
  136. stouputils/version_pkg.py +189 -0
  137. stouputils-1.14.0.dist-info/METADATA +178 -0
  138. stouputils-1.14.0.dist-info/RECORD +140 -0
  139. stouputils-1.14.0.dist-info/WHEEL +4 -0
  140. stouputils-1.14.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,128 @@
1
+ """
2
+ This module provides configuration settings and constants for the upscaler application.
3
+
4
+ It defines the URLs for downloading dependencies like waifu2x-ncnn-vulkan and FFmpeg,
5
+ and provides a Config class with settings for upscaling operations, including output quality,
6
+ bitrates, executable paths, and command-line arguments for the underlying tools.
7
+
8
+ Configuration options include:
9
+ - Image quality settings (JPG quality)
10
+ - Video encoding parameters (bitrate, codec, etc.)
11
+ - Paths to external binaries (FFmpeg, waifu2x-ncnn-vulkan)
12
+ - Command-line arguments for upscaling and video processing
13
+ """
14
+ # Constants
15
+ WAIFU2X_NCNN_VULKAN_RELEASES: dict[str, str] = {
16
+ "Windows": "https://github.com/nihui/waifu2x-ncnn-vulkan/releases/download/20220728/waifu2x-ncnn-vulkan-20220728-windows.zip",
17
+ "Linux": "https://github.com/nihui/waifu2x-ncnn-vulkan/releases/download/20220728/waifu2x-ncnn-vulkan-20220728-ubuntu.zip",
18
+ "Darwin": "https://github.com/nihui/waifu2x-ncnn-vulkan/releases/download/20220728/waifu2x-ncnn-vulkan-20220728-macos.zip"
19
+ }
20
+ """ URLs to download waifu2x-ncnn-vulkan from for each common platform. """
21
+ FFMPEG_RELEASES: dict[str, str] = {
22
+ "Windows": "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip",
23
+ "Linux": "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linux64-gpl.tar.xz",
24
+ "Darwin": "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-macos64-gpl.tar.xz"
25
+ }
26
+ """ URLs to download FFmpeg from for each common platform. """
27
+ YOUTUBE_BITRATE_RECOMMENDATIONS: dict[str, dict[str, dict[int, int]]] = {
28
+ "SDR": {
29
+ "standard": { # Standard Frame Rate (24, 25, 30)
30
+ 4320: 160000, # 8K - 160 Mbps
31
+ 2160: 45000, # 4K - 45 Mbps
32
+ 1440: 16000, # 2K - 16 Mbps
33
+ 1080: 8000, # 1080p - 8 Mbps
34
+ 720: 5000, # 720p - 5 Mbps
35
+ 480: 2500, # 480p - 2.5 Mbps
36
+ 0: 1000 # 360p or lower - 1 Mbps
37
+ },
38
+ "high": { # High Frame Rate (48, 50, 60)
39
+ 4320: 240000, # 8K - 240 Mbps
40
+ 2160: 68000, # 4K - 68 Mbps
41
+ 1440: 24000, # 2K - 24 Mbps
42
+ 1080: 12000, # 1080p - 12 Mbps
43
+ 720: 7500, # 720p - 7.5 Mbps
44
+ 480: 4000, # 480p - 4 Mbps
45
+ 0: 1500 # 360p or lower - 1.5 Mbps
46
+ }
47
+ },
48
+ "HDR": {
49
+ "standard": { # Standard Frame Rate (24, 25, 30)
50
+ 4320: 200000, # 8K - 200 Mbps
51
+ 2160: 56000, # 4K - 56 Mbps
52
+ 1440: 20000, # 2K - 20 Mbps
53
+ 1080: 10000, # 1080p - 10 Mbps
54
+ 0: 6500 # 720p or lower - 6.5 Mbps
55
+ },
56
+ "high": { # High Frame Rate (48, 50, 60)
57
+ 4320: 300000, # 8K - 300 Mbps
58
+ 2160: 85000, # 4K - 85 Mbps
59
+ 1440: 30000, # 2K - 30 Mbps
60
+ 1080: 15000, # 1080p - 15 Mbps
61
+ 0: 9500 # 720p or lower - 9.5 Mbps
62
+ }
63
+ }
64
+ }
65
+ """ YouTube bitrate recommendations for different resolutions and frame rates.
66
+
67
+ This dictionary contains recommended bitrates for YouTube uploads based on:
68
+ - SDR vs HDR content
69
+ - Standard frame rate (24, 25, 30 fps) vs high frame rate (48, 50, 60 fps)
70
+ - Video resolution (from 360p up to 8K)
71
+
72
+ The values are in kbps (kilobits per second).
73
+
74
+ Source: https://support.google.com/youtube/answer/1722171
75
+ """
76
+
77
+ # Configuration class
78
+ class Config:
79
+ """ Configuration class for the upscaler. """
80
+ JPG_QUALITY: int = 95
81
+ """ JPG quality for the output images. (Range: 0-100) """
82
+
83
+ VIDEO_FINAL_BITRATE: int = -1
84
+ """ Video final bitrate for the output video. -1 for YouTube recommended bitrate based on the video resolution. """
85
+
86
+ FFMPEG_EXECUTABLE: str = "ffmpeg"
87
+ """ Path to the ffmpeg executable, default is "ffmpeg" in the PATH. """
88
+
89
+ FFMPEG_ARGS: tuple[str, ...] = (
90
+ "-c:a", "copy", # Copy the audio without re-encoding
91
+ "-c:v", "hevc_nvenc", # Encode the video
92
+ "-map", "0:v:0", # Map the first input -i (frames) as video
93
+ "-map", "1:a:0?", # Map the second input -i (sound) as audio, with '?' to ignore if no audio stream
94
+ "-preset", "slow", # Set the encoding preset to slow (slower but better quality)
95
+ "-y", # Overwrite the output file if it exists
96
+ )
97
+ """ Additional arguments sent to the ffmpeg executable when calling subprocess.run(). """
98
+
99
+ FFPROBE_EXECUTABLE: str = "ffprobe"
100
+ """ Path to the ffprobe executable, default is "ffprobe" in the PATH. Used to get the framerate of the video. """
101
+
102
+ FFMPEG_CHECK_HELP_TEXT: str = "usage: ffmpeg [options]"
103
+ """ If this text is present in the output of the ffmpeg executable, it means it's installed correctly. """
104
+
105
+ UPSCALER_EXECUTABLE: str = "waifu2x-ncnn-vulkan"
106
+ """ Path to the upscaler executable, default is "waifu2x-ncnn-vulkan" in the PATH. """
107
+
108
+ UPSCALER_ARGS: tuple[str, ...] = (
109
+ "-i", "INPUT_PATH", # Input file path
110
+ "-o", "OUTPUT_PATH", # Output file path
111
+ "-s", "UPSCALE_RATIO", # Upscaling ratio
112
+ "-n", "3", # Noise level
113
+ )
114
+ """ Arguments sent to the upscaler executable when calling subprocess.run(). """
115
+
116
+ UPSCALER_EXECUTABLE_HELP_TEXT: str = "Usage: waifu2x-ncnn-vulkan -i"
117
+ """ If this text is present in the output of the upscaler executable, it means it's installed correctly. """
118
+
119
+ SLIGHTLY_FASTER_MODE: bool = False
120
+ """ If True, the upscaler executable will be called once, which is slightly faster but you can't see the progress. """
121
+
122
+ # Variables
123
+ upscaler_executable_checked: bool = False
124
+ """ Whether the upscaler executable has been checked for. """
125
+
126
+ ffmpeg_executable_checked: bool = False
127
+ """ Whether the ffmpeg executable has been checked for. """
128
+
@@ -0,0 +1,247 @@
1
+ """
2
+ This module provides utility functions for upscaling images using waifu2x-ncnn-vulkan.
3
+
4
+ It includes functions to upscale individual images, batches of images in a folder,
5
+ and handle intermediate operations like converting between image formats.
6
+ The module also manages temporary directories for partial processing and tracks
7
+ progress of batch operations.
8
+
9
+ Main functionalities:
10
+
11
+ - Converting frames between image formats (PNG to JPG)
12
+ - Upscaling individual images with configurable upscale ratio
13
+ - Batch processing folders of images with progress tracking
14
+ - Handling already processed images to resume interrupted operations
15
+
16
+ Example usage:
17
+
18
+ .. code-block:: python
19
+
20
+ from stouputils.applications.upscaler import upscale, upscale_folder
21
+
22
+ # Upscale a single image
23
+ upscale("input.jpg", "output.jpg", 2)
24
+
25
+ # Upscale a folder of images
26
+ upscale_folder("input_folder", "output_folder", 2)
27
+ """
28
+
29
+ # Imports
30
+ import os
31
+ import shutil
32
+ import subprocess
33
+ from tempfile import TemporaryDirectory
34
+
35
+ from PIL import Image
36
+
37
+ from ...installer import check_executable
38
+ from ...io import clean_path
39
+ from ...parallel import multithreading
40
+ from ...print import colored_for_loop, debug, info
41
+ from .config import WAIFU2X_NCNN_VULKAN_RELEASES, Config
42
+
43
+
44
+ # Function to convert a PNG frame to JPG format
45
+ def convert_frame(frame_path: str, delete_png: bool = True) -> None:
46
+ """ Convert a PNG frame to JPG format to take less space.
47
+
48
+ Args:
49
+ frame_path (str): Path to the PNG frame to convert.
50
+ delete_png (bool): Whether to delete the original PNG file after conversion.
51
+
52
+ Returns:
53
+ None: This function doesn't return anything.
54
+
55
+ Example:
56
+ .. code-block:: python
57
+
58
+ > convert_frame("input.png", delete_png=True)
59
+ > # input.png will be converted to input.jpg and the original file will be deleted
60
+
61
+ > convert_frame("input.png", delete_png=False)
62
+ > # input.png will be converted to input.jpg and the original file will be kept
63
+ """
64
+ if frame_path.endswith(".png"):
65
+ with Image.open(frame_path) as img:
66
+ img.save(frame_path.replace(".png", ".jpg"), quality=Config.JPG_QUALITY)
67
+ if delete_png:
68
+ os.remove(frame_path)
69
+
70
+
71
+ # Function to get all frames in a folder
72
+ def get_all_files(folder: str, suffix: str | tuple[str, ...] = "") -> list[str]:
73
+ """ Get all files paths in a folder, with a specific suffix if provided.
74
+
75
+ Args:
76
+ folder (str): Path to the folder containing the files.
77
+ suffix (str | tuple[str, ...]): Suffix of the files to get (e.g. ".png", ".jpg", etc.).
78
+
79
+ Returns:
80
+ list[str]: List of all files paths in the folder.
81
+
82
+ Example:
83
+ >>> files: list[str] = get_all_files("some_folder", ".png")
84
+ >>> len(files)
85
+ 0
86
+ """
87
+ if not os.path.exists(folder):
88
+ return []
89
+ return [f"{folder}/{f}" for f in os.listdir(folder) if f.endswith(suffix)]
90
+
91
+
92
+ # Function to create a temporary directory with not upscaled images
93
+ def create_temp_dir_for_not_upscaled(input_path: str, output_path: str) -> TemporaryDirectory[str] | None:
94
+ """ Creates a temporary directory containing only images that haven't been upscaled yet.
95
+
96
+ Args:
97
+ input_path (str): Path to the folder containing input images.
98
+ output_path (str): Path to the folder where upscaled images are saved.
99
+
100
+ Returns:
101
+ TemporaryDirectory[str] | None: A temporary directory object if there are images to process,
102
+ None if all images are already upscaled.
103
+ """
104
+ # Get all input images and the not upscaled images
105
+ all_inputs: list[str] = get_all_files(input_path)
106
+ not_upscaled_images: list[str] = [x for x in all_inputs if not os.path.exists(f"{output_path}/{os.path.basename(x)}")]
107
+
108
+ # If all images or none are already upscaled, return None
109
+ if len(not_upscaled_images) == 0 or (len(not_upscaled_images) == len(all_inputs)):
110
+ return None
111
+
112
+ # Create a temporary directory and copy the not upscaled images to it
113
+ temp_dir: TemporaryDirectory[str] = TemporaryDirectory()
114
+ debug(f"Creating temporary directory to process {len(not_upscaled_images)} images: {temp_dir.name}")
115
+ for image in not_upscaled_images:
116
+ shutil.copyfile(image, f"{temp_dir.name}/{os.path.basename(image)}")
117
+ return temp_dir
118
+
119
+
120
+ # Helper function to check if the upscaler executable is installed
121
+ def check_upscaler_executable() -> None:
122
+ if not Config.upscaler_executable_checked:
123
+ check_executable(Config.UPSCALER_EXECUTABLE, Config.UPSCALER_EXECUTABLE_HELP_TEXT, WAIFU2X_NCNN_VULKAN_RELEASES)
124
+ Config.upscaler_executable_checked = True
125
+
126
+
127
+ # Function to upscale an image
128
+ def upscale(input_path: str, output_path: str, upscale_ratio: int) -> None:
129
+ """ Upscale an input image (or a directory of images) with the upscaler executable.
130
+
131
+ Args:
132
+ input_path (str): Path to the image to upscale (or a directory).
133
+ output_path (str): Path to the output image (or a directory).
134
+ upscale_ratio (int): Upscaling ratio.
135
+
136
+ Example:
137
+ .. code-block:: python
138
+
139
+ > upscale("folder", "folder", 2)
140
+ Traceback (most recent call last):
141
+ ...
142
+ AssertionError: Input and output paths cannot be the same, got 'folder'
143
+
144
+ > upscale("stouputils", "stouputils/output.jpg", 2)
145
+ Traceback (most recent call last):
146
+ ...
147
+ AssertionError: If input is a directory, output must be a directory too, got 'stouputils/output.jpg'
148
+
149
+
150
+ > upscale("input.jpg", "output.jpg", 2)
151
+ > # The input.jpg will be upscaled to output.jpg with a ratio of 2
152
+
153
+ > upscale("input_folder", "output_folder", 2)
154
+ > # The input_folder will be upscaled to output_folder with a ratio of 2
155
+ """
156
+ check_upscaler_executable()
157
+ is_input_dir: bool = os.path.isdir(input_path)
158
+ is_output_dir: bool = os.path.isdir(output_path)
159
+
160
+ # Assertions
161
+ assert input_path != output_path, f"Input and output paths cannot be the same, got '{input_path}'"
162
+ invalid_dir_combination: bool = is_input_dir == True and is_output_dir == False # noqa: E712
163
+ assert not invalid_dir_combination, f"If input is a directory, output must be a directory too, got '{output_path}'"
164
+
165
+ # Convert output_path to a file path if it's a directory
166
+ if is_output_dir and not is_input_dir:
167
+
168
+ # Needs to be a PNG file to be converted to JPG later
169
+ output_file_name: str = os.path.basename(input_path).replace(".jpg", ".png")
170
+ output_path = clean_path(f"{output_path}/{output_file_name}")
171
+ is_output_dir = False
172
+
173
+ # If both input and output are folders, and there are images already upscaled in the output folder,
174
+ # Then create a temporary folder with not upscaled images
175
+ temp_dir: TemporaryDirectory[str] | None = None
176
+ if is_input_dir and is_output_dir:
177
+ temp_dir = create_temp_dir_for_not_upscaled(input_path, output_path)
178
+ if temp_dir:
179
+ input_path = temp_dir.name
180
+
181
+ # Build the command and run it
182
+ cmd: list[str] = [Config.UPSCALER_EXECUTABLE, *Config.UPSCALER_ARGS]
183
+ cmd[cmd.index("INPUT_PATH")] = input_path # Replace the input path
184
+ cmd[cmd.index("OUTPUT_PATH")] = output_path # Replace the output path
185
+ cmd[cmd.index("UPSCALE_RATIO")] = str(upscale_ratio) # Replace the upscaled ratio (if using waifu2x-ncnn-vulkan)
186
+ subprocess.run(cmd, capture_output=True)
187
+
188
+ # If the input was a temporary folder, delete it
189
+ if temp_dir:
190
+ temp_dir.cleanup()
191
+
192
+ # Convert the output frames to JPG format
193
+ if not is_output_dir:
194
+ convert_frame(output_path)
195
+ else:
196
+ frames_to_convert: list[str] = get_all_files(output_path, ".png")
197
+ if frames_to_convert:
198
+ multithreading(convert_frame, frames_to_convert, desc="Converting frames to JPG format")
199
+
200
+
201
+ # Function to upscale multiple images
202
+ def upscale_images(images: list[str], output_folder: str, upscale_ratio: int, desc: str = "Upscaling images") -> None:
203
+ """ Upscale multiple images from a list.
204
+
205
+ Args:
206
+ images (list[str]): List of paths to the images to upscale.
207
+ output_folder (str): Path to the output folder where the upscaled images will be saved.
208
+ upscale_ratio (int): Upscaling ratio.
209
+ desc (str): Description of the function execution displayed in the progress bar.
210
+ No progress bar will be displayed if desc is empty.
211
+
212
+ Returns:
213
+ None: This function doesn't return anything.
214
+ """
215
+ for image_path in (colored_for_loop(images, desc=desc) if desc != "" else images):
216
+ upscale(image_path, output_folder, upscale_ratio)
217
+
218
+ # Function to upscale a folder of images
219
+ def upscale_folder(
220
+ input_folder: str,
221
+ output_folder: str,
222
+ upscale_ratio: int,
223
+ slightly_faster_mode: bool = True,
224
+ desc: str = "Upscaling folder"
225
+ ) -> None:
226
+ """ Upscale all images in a folder.
227
+
228
+ Args:
229
+ input_folder (str): Path to the input folder containing the images to upscale.
230
+ output_folder (str): Path to the output folder where the upscaled images will be saved.
231
+ upscale_ratio (int): Upscaling ratio.
232
+ slightly_faster_mode (bool): Whether to use the slightly faster mode (no progress bar),
233
+ one call to the upscaler executable.
234
+ desc (str): Description of the function execution displayed in the progress bar.
235
+ No progress bar will be displayed if desc is empty.
236
+
237
+ Returns:
238
+ None: This function doesn't return anything.
239
+ """
240
+ info(f"Upscaling '{input_folder}' to '{output_folder}' with a ratio of {upscale_ratio}...")
241
+ if slightly_faster_mode:
242
+ upscale(input_folder, output_folder, upscale_ratio)
243
+ else:
244
+ inputs: list[str] = get_all_files(input_folder)
245
+ not_upscaled_images: list[str] = [x for x in inputs if not os.path.exists(f"{output_folder}/{os.path.basename(x)}")]
246
+ upscale_images(not_upscaled_images, output_folder, upscale_ratio, desc=desc)
247
+
@@ -0,0 +1,287 @@
1
+ """
2
+ This module provides utility functions for upscaling videos using waifu2x-ncnn-vulkan.
3
+
4
+ It extracts frames from videos, upscales them individually, and then recombines them
5
+ into a high-quality output video. The process preserves audio from the original video
6
+ and handles configuration of video encoding parameters like bitrate and framerate.
7
+
8
+ Main functionalities:
9
+
10
+ - Extracting frames from videos using FFmpeg
11
+ - Upscaling frames using waifu2x-ncnn-vulkan
12
+ - Recombining frames into videos with optimized bitrates
13
+ - Handling partially processed videos to resume interrupted operations
14
+ - Calculating recommended bitrates based on resolution and framerate
15
+
16
+ The module includes YouTube's recommended bitrate settings for different resolutions,
17
+ framerates, and HDR/SDR content types, ensuring optimal quality for various outputs.
18
+
19
+ Example usage:
20
+
21
+ .. code-block:: python
22
+
23
+ # Imports
24
+ import stouputils.applications.upscaler as app
25
+ from stouputils.io import get_root_path
26
+
27
+ # Constants
28
+ ROOT: str = get_root_path(__file__) + "/upscaler"
29
+ INPUT_FOLDER: str = f"{ROOT}/input"
30
+ PROGRESS_FOLDER: str = f"{ROOT}/progress"
31
+ OUTPUT_FOLDER: str = f"{ROOT}/output"
32
+
33
+ # Main
34
+ if __name__ == "__main__":
35
+ app.video_upscaler_cli(INPUT_FOLDER, PROGRESS_FOLDER, OUTPUT_FOLDER)
36
+ """
37
+
38
+ # Imports
39
+ import os
40
+ import shutil
41
+ import subprocess
42
+ import sys
43
+ from typing import Literal
44
+
45
+ from PIL import Image
46
+
47
+ from ...installer import check_executable
48
+ from ...io import clean_path
49
+ from ...parallel import multithreading
50
+ from ...print import colored_for_loop, debug, error, info, warning
51
+ from .config import FFMPEG_RELEASES, YOUTUBE_BITRATE_RECOMMENDATIONS, Config
52
+ from .image import convert_frame, get_all_files, upscale_folder
53
+
54
+
55
+ # Functions
56
+ def get_recommended_bitrate(
57
+ resolution: tuple[int, int], frame_rate: int = 60, upload_type: Literal["SDR","HDR"] = "SDR"
58
+ ) -> int:
59
+ """ Get the recommended bitrate (in kbps) for the output video based on the video resolution.
60
+
61
+ Args:
62
+ resolution (tuple[int, int]): Video resolution (width, height).
63
+ frame_rate (int): Frame rate of the video, default is 60.
64
+ upload_type (Literal["SDR","HDR"]): Upload type from which the recommendation is made, default is "SDR".
65
+
66
+ Returns:
67
+ int: The recommended bitrate for the output video (in kbps)
68
+
69
+ Source: https://support.google.com/youtube/answer/1722171?hl=en#zippy=%2Cbitrate
70
+
71
+ Examples:
72
+ >>> # Valid examples
73
+ >>> get_recommended_bitrate((3840, 2160), 60, "SDR")
74
+ 68000
75
+ >>> get_recommended_bitrate((1920, 1080), 60, "HDR")
76
+ 15000
77
+ >>> get_recommended_bitrate((1920, 1080), 60, "SDR")
78
+ 12000
79
+ >>> get_recommended_bitrate((1920, 1080), 30, "SDR")
80
+ 8000
81
+
82
+ >>> # Invalid examples
83
+ >>> get_recommended_bitrate((1920, 1080), 60, "Ratio")
84
+ Traceback (most recent call last):
85
+ ...
86
+ AssertionError: Invalid upload type: 'Ratio'
87
+ >>> get_recommended_bitrate("1920x1080", 60, "SDR")
88
+ Traceback (most recent call last):
89
+ ...
90
+ AssertionError: Invalid resolution: 1920x1080, must be a tuple of two integers
91
+ >>> get_recommended_bitrate((1920, 1080), -10, "SDR")
92
+ Traceback (most recent call last):
93
+ ...
94
+ AssertionError: Invalid frame rate: -10, must be a positive integer
95
+ """
96
+ # Assertions
97
+ assert isinstance(resolution, tuple) and len(resolution) == 2, \
98
+ f"Invalid resolution: {resolution}, must be a tuple of two integers"
99
+ assert isinstance(frame_rate, int) and frame_rate > 0, \
100
+ f"Invalid frame rate: {frame_rate}, must be a positive integer"
101
+ assert upload_type in YOUTUBE_BITRATE_RECOMMENDATIONS, \
102
+ f"Invalid upload type: '{upload_type}'"
103
+
104
+ # Determine frame rate category
105
+ frame_rate_category: str = "high" if frame_rate >= 48 else "standard"
106
+
107
+ # Get the appropriate bitrate dictionary
108
+ resolution_bitrates: dict[int, int] = YOUTUBE_BITRATE_RECOMMENDATIONS[upload_type][frame_rate_category]
109
+
110
+ # Find the appropriate bitrate based on resolution
111
+ max_dimension: int = min(*resolution)
112
+ for min_resolution, bitrate in sorted(resolution_bitrates.items(), reverse=True):
113
+ if max_dimension >= min_resolution:
114
+ return bitrate
115
+
116
+ # Fallback (should never reach here due to the '0' key in dictionaries)
117
+ return 1000
118
+
119
+
120
+ def check_ffmpeg_executable() -> None:
121
+ if not Config.ffmpeg_executable_checked:
122
+ check_executable(Config.FFMPEG_EXECUTABLE, Config.FFMPEG_CHECK_HELP_TEXT, FFMPEG_RELEASES, append_to_path="bin")
123
+ Config.ffmpeg_executable_checked = True
124
+
125
+ # Routine to handle a video file
126
+ def upscale_video(video_file: str, input_folder: str, progress_folder: str, output_folder: str) -> None:
127
+ """ Handles a video file. """
128
+ # Prepare paths
129
+ input_path: str = f"{input_folder}/{video_file}"
130
+ progress_path: str = f"{progress_folder}/{video_file}"
131
+ p_extracted_path: str = f"{progress_path}/extracted"
132
+ p_upscaled_path: str = f"{progress_path}/upscaled"
133
+ output_path: str = f"{output_folder}/{video_file}"
134
+ os.makedirs(p_extracted_path, exist_ok = True)
135
+ os.makedirs(p_upscaled_path, exist_ok = True)
136
+
137
+ # Check if executable is installed
138
+ check_ffmpeg_executable()
139
+
140
+ ## Step 1: Check if the video file is already upscaled or partially processed, if not, extract frames
141
+ # If the video file is already upscaled, skip it
142
+ if os.path.exists(output_path):
143
+ warning(f"'{video_file}' has already been processed, remove it from the output folder to reprocess it.")
144
+ return
145
+
146
+ # If the video is already in the list of videos that have been partially processed, ask to restart or skip
147
+ is_partially_processed: bool = len(os.listdir(p_extracted_path)) > 0 and len(os.listdir(p_upscaled_path)) > 0
148
+ if is_partially_processed:
149
+ info(f"'{video_file}' has already been partially processed, do you want to resume the process? (Y/n)")
150
+ if input().lower() == "n":
151
+ shutil.rmtree(p_upscaled_path, ignore_errors = True)
152
+ os.makedirs(p_upscaled_path, exist_ok = True)
153
+ is_partially_processed = False
154
+
155
+ # If the video is not partially processed, extract frames
156
+ if not is_partially_processed:
157
+ debug(f"Extracting frames from '{video_file}'...")
158
+
159
+ # Extract frames using ffmpeg
160
+ command: list[str] = [Config.FFMPEG_EXECUTABLE, "-i", input_path, f"{p_extracted_path}/%09d.png"]
161
+ subprocess.run(command, capture_output = True)
162
+
163
+ # Convert all frames to JPG format
164
+ frames_to_convert: list[str] = get_all_files(p_extracted_path, ".png")
165
+ if frames_to_convert:
166
+ multithreading(convert_frame, frames_to_convert, desc="Converting frames to JPG format")
167
+
168
+
169
+ ## Step 2: Upscale the frames
170
+ # Get all the frames in the progress folder
171
+ all_frames: list[str] = get_all_files(p_extracted_path, ".jpg")
172
+ upscaled_frames: list[str] = get_all_files(p_upscaled_path, ".jpg")
173
+
174
+ # If there are frames to upscale,
175
+ if len(all_frames) > len(upscaled_frames):
176
+
177
+ # Try to get upscaling ratio if any
178
+ upscale_ratio: int = 2
179
+ if upscaled_frames:
180
+ with Image.open(upscaled_frames[0]) as img:
181
+ upscaled_size: tuple[int, int] = img.size
182
+ with Image.open(all_frames[0]) as img:
183
+ extracted_size: tuple[int, int] = img.size
184
+ upscale_ratio = upscaled_size[0] // extracted_size[0]
185
+ info(f"Detected upscaling ratio: {upscale_ratio}")
186
+ else:
187
+ if "--upscale" in sys.argv:
188
+ upscale_index: int = sys.argv.index("--upscale")
189
+ if upscale_index + 1 < len(sys.argv):
190
+ upscale_ratio = int(sys.argv[upscale_index + 1])
191
+ else:
192
+ error(
193
+ "No upscaling ratio provided with --upscale flag. "
194
+ "Please provide a ratio after the flag. (1/2/4/8/16/32)",
195
+ exit=True
196
+ )
197
+ else:
198
+ info("No upscaling ratio provided, please enter one (1/2/4/8/16/32, default=2):")
199
+ upscale_ratio = int(input() or "2")
200
+
201
+ # For each frame that hasn't been upscaled yet, upscale it
202
+ upscale_folder(p_extracted_path, p_upscaled_path, upscale_ratio, slightly_faster_mode=Config.SLIGHTLY_FASTER_MODE)
203
+
204
+ ## Step 3: Convert the upscaled frames to a video
205
+ # Get the video bitrate
206
+ if Config.VIDEO_FINAL_BITRATE == -1:
207
+ upscaled_frame: str = get_all_files(p_upscaled_path, ".jpg")[0]
208
+ with Image.open(upscaled_frame) as img:
209
+ upscaled_size: tuple[int, int] = img.size
210
+ video_bitrate: int = get_recommended_bitrate(upscaled_size)
211
+ else:
212
+ video_bitrate: int = Config.VIDEO_FINAL_BITRATE
213
+
214
+ # Get the framerate of the original video
215
+ original_framerate: str = "60"
216
+ ffprobe_command: list[str] = [
217
+ Config.FFPROBE_EXECUTABLE, # Path to the ffprobe executable
218
+ "-v", "error", # Set verbosity level to error (only show errors)
219
+ "-select_streams", "v:0", # Select the first video stream
220
+ "-show_entries", "stream=r_frame_rate", # Show only the frame rate information
221
+ "-of", "default=noprint_wrappers=1:nokey=1", # Format output without wrappers and keys
222
+ input_path, # Path to the input video file
223
+ ]
224
+ try:
225
+ result = subprocess.run(ffprobe_command, capture_output=True, text=True, check=True)
226
+ framerate: str = result.stdout.strip()
227
+ if not framerate or '/' not in framerate: # Basic validation
228
+ warning(f"Could not reliably determine framerate for '{video_file}'. Falling back to 60.")
229
+ original_framerate = "60"
230
+ else:
231
+ debug(f"Detected original framerate: {framerate}")
232
+ original_framerate = framerate
233
+ except (subprocess.CalledProcessError, FileNotFoundError) as e:
234
+ warning(f"Failed to get framerate using ffprobe for '{video_file}': {e}. Falling back to 60.")
235
+
236
+
237
+ # Prepare the command to convert the upscaled frames to a video
238
+ subprocess.run([
239
+ Config.FFMPEG_EXECUTABLE,
240
+ "-framerate", original_framerate, # Use the original video's framerate for input frames
241
+ "-i", f"{p_upscaled_path}/%09d.jpg", # Use p_upscaled_path, not upscaled_path
242
+ "-i", input_path, # Input video for sound and metadata
243
+ "-b:v", f"{video_bitrate}k", # Set the video bitrate (in kbps)
244
+ *Config.FFMPEG_ARGS, # Additional arguments from the config
245
+ "-r", original_framerate, # Set the *output* video framerate
246
+ output_path, # Output video
247
+ ])
248
+
249
+
250
+ def video_upscaler_cli(input_folder: str, progress_folder: str, output_folder: str) -> None:
251
+ """ Upscales videos from an input folder and saves them to an output folder.
252
+
253
+ Uses intermediate folders for extracted and upscaled frames within the progress folder.
254
+ **Handles resuming partially processed videos.**
255
+
256
+ Args:
257
+ input_folder (str): Path to the folder containing input videos.
258
+ progress_folder (str): Path to the folder for storing intermediate files (frames).
259
+ output_folder (str): Path to the folder where upscaled videos will be saved.
260
+ """
261
+ # Clean paths
262
+ input_folder = clean_path(input_folder)
263
+ progress_folder = clean_path(progress_folder)
264
+ output_folder = clean_path(output_folder)
265
+ os.makedirs(input_folder, exist_ok = True)
266
+ os.makedirs(progress_folder, exist_ok = True)
267
+ os.makedirs(output_folder, exist_ok = True)
268
+
269
+ # Ask if we should shutdown the computer after the script is finished
270
+ info("Do you want to shutdown the computer after the script is finished? (y/N)")
271
+ shutdown_after_script: bool = input().lower() == "y"
272
+
273
+ # Collect all video files in the input folder
274
+ videos: list[str] = [file for file in os.listdir(input_folder) if not file.endswith(".md")]
275
+
276
+ # Handle each video file
277
+ for video in colored_for_loop(videos, desc="Upscaling videos"):
278
+ upscale_video(video, input_folder, progress_folder, output_folder)
279
+
280
+ # Shutdown the computer after the script is finished
281
+ if shutdown_after_script:
282
+ info("Shutting down the computer...")
283
+ if os.name == "nt":
284
+ subprocess.run(["shutdown", "/s", "/t", "0", "/f"], capture_output = False)
285
+ else:
286
+ subprocess.run(["shutdown", "now"], capture_output = False)
287
+