movie-barcodes 0.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of movie-barcodes might be problematic. Click here for more details.
- movie_barcodes-0.0.2.dist-info/LICENSE +339 -0
- movie_barcodes-0.0.2.dist-info/METADATA +134 -0
- movie_barcodes-0.0.2.dist-info/RECORD +18 -0
- movie_barcodes-0.0.2.dist-info/WHEEL +5 -0
- movie_barcodes-0.0.2.dist-info/entry_points.txt +2 -0
- movie_barcodes-0.0.2.dist-info/top_level.txt +2 -0
- src/__init__.py +0 -0
- src/barcode_generation.py +70 -0
- src/color_extraction.py +87 -0
- src/main.py +181 -0
- src/utility.py +168 -0
- src/video_processing.py +149 -0
- tests/__init__.py +0 -0
- tests/test_barcode_generation.py +58 -0
- tests/test_color_extraction.py +39 -0
- tests/test_integration.py +88 -0
- tests/test_utility.py +320 -0
- tests/test_video_processing.py +232 -0
src/main.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import logging
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
from typing import Callable
|
|
6
|
+
from os import cpu_count, path
|
|
7
|
+
|
|
8
|
+
from .barcode_generation import generate_circular_barcode, generate_barcode
|
|
9
|
+
|
|
10
|
+
from .utility import (
|
|
11
|
+
save_barcode_image,
|
|
12
|
+
get_dominant_color_function,
|
|
13
|
+
format_time,
|
|
14
|
+
get_video_properties,
|
|
15
|
+
validate_args,
|
|
16
|
+
)
|
|
17
|
+
from .video_processing import load_video, extract_colors, parallel_extract_colors
|
|
18
|
+
|
|
19
|
+
MAX_PROCESSES = cpu_count() or 1
|
|
20
|
+
MIN_FRAME_COUNT = 2
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def generate_and_save_barcode(args: argparse.Namespace, dominant_color_function: Callable, method: str) -> None:
|
|
24
|
+
"""
|
|
25
|
+
Generate and save the barcode image based on the specified method.
|
|
26
|
+
|
|
27
|
+
:param args: argparse.Namespace object containing the command-line arguments
|
|
28
|
+
:param dominant_color_function: The function to extract the dominant color from a frame
|
|
29
|
+
:param method: The method used to extract the dominant color
|
|
30
|
+
:return: None
|
|
31
|
+
"""
|
|
32
|
+
start_time = time.time()
|
|
33
|
+
|
|
34
|
+
# Get Video Properties
|
|
35
|
+
video, frame_count, frame_width, frame_height = load_video(args.input_video_path)
|
|
36
|
+
_, _, video_duration, video_size = get_video_properties(video, args)
|
|
37
|
+
|
|
38
|
+
# If the user specifies the 'workers' argument
|
|
39
|
+
if args.workers is not None:
|
|
40
|
+
if args.workers == 1:
|
|
41
|
+
# If the user explicitly sets 'workers' to 1, use sequential processing
|
|
42
|
+
colors = extract_colors(
|
|
43
|
+
args.input_video_path,
|
|
44
|
+
0,
|
|
45
|
+
frame_count - 1,
|
|
46
|
+
dominant_color_function,
|
|
47
|
+
args.width,
|
|
48
|
+
)
|
|
49
|
+
else:
|
|
50
|
+
# Perform parallel processing with the user-specified number of workers
|
|
51
|
+
colors = parallel_extract_colors(
|
|
52
|
+
args.input_video_path,
|
|
53
|
+
frame_count,
|
|
54
|
+
dominant_color_function,
|
|
55
|
+
args.workers,
|
|
56
|
+
args.width,
|
|
57
|
+
)
|
|
58
|
+
else:
|
|
59
|
+
# If 'workers' is not specified, use the maximum number of available CPU cores
|
|
60
|
+
colors = parallel_extract_colors(
|
|
61
|
+
args.input_video_path,
|
|
62
|
+
frame_count,
|
|
63
|
+
dominant_color_function,
|
|
64
|
+
MAX_PROCESSES,
|
|
65
|
+
args.width,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Generate the appropriate type of barcode
|
|
69
|
+
if args.barcode_type == "circular":
|
|
70
|
+
# Assuming image width = video frame width for circular barcodes
|
|
71
|
+
barcode = generate_circular_barcode(colors, frame_width)
|
|
72
|
+
else:
|
|
73
|
+
barcode = generate_barcode(colors, frame_height, frame_count, args.width)
|
|
74
|
+
|
|
75
|
+
base_name = path.basename(args.input_video_path)
|
|
76
|
+
file_name_without_extension = path.splitext(base_name)[0]
|
|
77
|
+
save_barcode_image(barcode, file_name_without_extension, args, method)
|
|
78
|
+
|
|
79
|
+
# Calculate processing time
|
|
80
|
+
end_time = time.time()
|
|
81
|
+
processing_time = end_time - start_time
|
|
82
|
+
|
|
83
|
+
# Log the information
|
|
84
|
+
logging.info("Processed File: %s", file_name_without_extension)
|
|
85
|
+
logging.info("Number of Frames: %d", frame_count)
|
|
86
|
+
logging.info("Video Duration: %s", format_time(video_duration))
|
|
87
|
+
logging.info("Video Size: %.2f MB", video_size / (1024 * 1024))
|
|
88
|
+
logging.info("Processing Time: %s", format_time(processing_time))
|
|
89
|
+
|
|
90
|
+
video.release()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def main(args: argparse.Namespace) -> None:
|
|
94
|
+
"""
|
|
95
|
+
Main function to generate a barcode from a video file.
|
|
96
|
+
|
|
97
|
+
:param args: argparse.Namespace object containing the command-line arguments
|
|
98
|
+
:return: None
|
|
99
|
+
"""
|
|
100
|
+
# Check if the input video file exists
|
|
101
|
+
_, frame_count, _, _ = load_video(args.input_video_path)
|
|
102
|
+
|
|
103
|
+
# Check if the arguments are valid
|
|
104
|
+
validate_args(args, frame_count, MAX_PROCESSES, MIN_FRAME_COUNT)
|
|
105
|
+
|
|
106
|
+
# Get a list of all available methods
|
|
107
|
+
methods = ["avg", "hsv", "bgr", "kmeans"]
|
|
108
|
+
|
|
109
|
+
# Check if all_methods flag is set
|
|
110
|
+
if args.all_methods:
|
|
111
|
+
for method in methods:
|
|
112
|
+
# Generate barcodes for each method
|
|
113
|
+
dominant_color_function = get_dominant_color_function(method)
|
|
114
|
+
generate_and_save_barcode(args, dominant_color_function, method)
|
|
115
|
+
else:
|
|
116
|
+
# Use the specified method to generate barcode
|
|
117
|
+
dominant_color_function = get_dominant_color_function(args.method)
|
|
118
|
+
generate_and_save_barcode(args, dominant_color_function, args.method)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
if __name__ == "__main__":
|
|
122
|
+
logging.basicConfig(
|
|
123
|
+
filename=path.join("..", "logs.txt"),
|
|
124
|
+
level=logging.INFO,
|
|
125
|
+
format="%(asctime)s - %(message)s",
|
|
126
|
+
)
|
|
127
|
+
header_msg = "=" * 40 + " NEW RUN " + "=" * 40
|
|
128
|
+
logging.info("\n%s\n", header_msg)
|
|
129
|
+
|
|
130
|
+
parser = argparse.ArgumentParser(description="Generate a color barcode from a video file.")
|
|
131
|
+
parser.add_argument("--input_video_path", type=str, help="Path to the video file.")
|
|
132
|
+
parser.add_argument(
|
|
133
|
+
"--destination_path",
|
|
134
|
+
type=str,
|
|
135
|
+
nargs="?",
|
|
136
|
+
help="Path to save the output image. If not provided, the image will be saved in a default location.",
|
|
137
|
+
default=None,
|
|
138
|
+
)
|
|
139
|
+
parser.add_argument(
|
|
140
|
+
"--barcode_type",
|
|
141
|
+
choices=["horizontal", "circular"],
|
|
142
|
+
default="horizontal",
|
|
143
|
+
help="Type of barcode to generate: horizontal or circular. Default is horizontal.",
|
|
144
|
+
)
|
|
145
|
+
parser.add_argument(
|
|
146
|
+
"--method",
|
|
147
|
+
choices=["avg", "kmeans", "hsv", "bgr"],
|
|
148
|
+
default="avg",
|
|
149
|
+
help="Method to extract dominant color: avg (average), kmeans (K-Means clustering), hsv (HSV "
|
|
150
|
+
"histogram), or bgr (BGR histogram). Default is avg.",
|
|
151
|
+
)
|
|
152
|
+
parser.add_argument(
|
|
153
|
+
"--workers",
|
|
154
|
+
type=int,
|
|
155
|
+
default=None,
|
|
156
|
+
help="Number of workers for parallel processing. Default behavior uses all available CPU cores."
|
|
157
|
+
"Setting this to 1 will use sequential processing. Do not specify a value greater than "
|
|
158
|
+
"the number of available CPU cores.",
|
|
159
|
+
)
|
|
160
|
+
parser.add_argument(
|
|
161
|
+
"--width",
|
|
162
|
+
type=int,
|
|
163
|
+
default=None,
|
|
164
|
+
help="Width of the output image. If not provided, the width will be the same as the video",
|
|
165
|
+
)
|
|
166
|
+
parser.add_argument(
|
|
167
|
+
"--output_name",
|
|
168
|
+
type=str,
|
|
169
|
+
nargs="?",
|
|
170
|
+
help="Custom name for the output barcode image. If not provided, a name will be automatically " "generated.",
|
|
171
|
+
default=None,
|
|
172
|
+
)
|
|
173
|
+
parser.add_argument(
|
|
174
|
+
"--all_methods",
|
|
175
|
+
type=bool,
|
|
176
|
+
default=False,
|
|
177
|
+
help="If provided, all methods to extract dominant color will be used to create barcodes. "
|
|
178
|
+
"Overrides --method argument.",
|
|
179
|
+
)
|
|
180
|
+
arguments = parser.parse_args()
|
|
181
|
+
main(arguments)
|
src/utility.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from os import path, access, W_OK, makedirs
|
|
3
|
+
from typing import Callable
|
|
4
|
+
import cv2
|
|
5
|
+
import numpy as np
|
|
6
|
+
from PIL import Image
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
from .color_extraction import (
|
|
10
|
+
get_dominant_color_mean,
|
|
11
|
+
get_dominant_color_kmeans,
|
|
12
|
+
get_dominant_color_hsv,
|
|
13
|
+
get_dominant_color_bgr,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def validate_args(args: argparse.Namespace, frame_count: int, MAX_PROCESSES: int, MIN_FRAME_COUNT: int) -> None:
|
|
18
|
+
"""
|
|
19
|
+
Validate command-line arguments for logical errors.
|
|
20
|
+
|
|
21
|
+
:param argparse.Namespace args: The command-line arguments.
|
|
22
|
+
:param int frame_count: The number of frames in the video.
|
|
23
|
+
:param int MAX_PROCESSES: The maximum number of processes to use.
|
|
24
|
+
:param int MIN_FRAME_COUNT: The minimum number of frames required in the video.
|
|
25
|
+
:return: None
|
|
26
|
+
:raises FileNotFoundError: If the input video file does not exist.
|
|
27
|
+
:raises ValueError: If the video file has an invalid extension, the destination path is not writable, the number of
|
|
28
|
+
workers is invalid, the width is invalid, the frame count is invalid, or the method is invalid.
|
|
29
|
+
:raises PermissionError: If the destination path is not writable.
|
|
30
|
+
"""
|
|
31
|
+
# Check if input video file exists
|
|
32
|
+
if not path.exists(args.input_video_path):
|
|
33
|
+
raise FileNotFoundError(f"The specified input video file '{args.input_video_path}' does not exist.")
|
|
34
|
+
|
|
35
|
+
valid_extensions = [".mp4", ".webm"]
|
|
36
|
+
if path.splitext(args.input_video_path)[1].lower() not in valid_extensions:
|
|
37
|
+
raise ValueError("The specified video file must have a valid video extension (e.g., .mp4).")
|
|
38
|
+
|
|
39
|
+
# Check if the destination path is writable
|
|
40
|
+
if args.destination_path is not None:
|
|
41
|
+
destination_dir = path.dirname(args.destination_path)
|
|
42
|
+
if not access(destination_dir, W_OK):
|
|
43
|
+
raise PermissionError(f"The specified destination path '{args.destination_path}' is not writable.")
|
|
44
|
+
|
|
45
|
+
if args.workers is not None:
|
|
46
|
+
if args.workers < 1:
|
|
47
|
+
raise ValueError("The number of workers must be greater than or equal to 1.")
|
|
48
|
+
if args.workers > MAX_PROCESSES:
|
|
49
|
+
raise ValueError(
|
|
50
|
+
f"The number of workers specified ({args.workers}) exceeds "
|
|
51
|
+
f"the number of available CPU cores ({MAX_PROCESSES})."
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if args.width is not None:
|
|
55
|
+
if args.width <= 0:
|
|
56
|
+
raise ValueError("Width must be greater than 0.")
|
|
57
|
+
if args.width > frame_count:
|
|
58
|
+
raise ValueError(
|
|
59
|
+
f"Specified width ({args.width}) cannot be greater than the number of frames ({frame_count}) in the "
|
|
60
|
+
f"video."
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
if frame_count < MIN_FRAME_COUNT:
|
|
64
|
+
raise ValueError(f"The video must have at least {MIN_FRAME_COUNT} frames.")
|
|
65
|
+
|
|
66
|
+
if args.all_methods and args.method is not None:
|
|
67
|
+
raise ValueError("The --all_methods flag cannot be used with the --method argument.")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def get_dominant_color_function(method: str) -> Callable:
|
|
71
|
+
"""
|
|
72
|
+
Returns the appropriate function to get the dominant color based on the specified method.
|
|
73
|
+
|
|
74
|
+
:param str method: The method to use for color extraction ('avg', 'kmeans', 'hsv', or 'bgr').
|
|
75
|
+
:return: Function to get the dominant color.
|
|
76
|
+
:raises ValueError: If the method is invalid.
|
|
77
|
+
"""
|
|
78
|
+
if method == "avg":
|
|
79
|
+
# return get_dominant_color_avg
|
|
80
|
+
return get_dominant_color_mean
|
|
81
|
+
if method == "kmeans":
|
|
82
|
+
return get_dominant_color_kmeans
|
|
83
|
+
if method == "hsv":
|
|
84
|
+
return get_dominant_color_hsv
|
|
85
|
+
if method == "bgr":
|
|
86
|
+
return get_dominant_color_bgr
|
|
87
|
+
|
|
88
|
+
raise ValueError(f"Invalid method: {method}")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def format_time(seconds: float) -> str:
|
|
92
|
+
"""
|
|
93
|
+
Formats time in seconds to a string representation.
|
|
94
|
+
|
|
95
|
+
:param int seconds: The time in seconds.
|
|
96
|
+
:return: Formatted time string.
|
|
97
|
+
"""
|
|
98
|
+
hours, remainder = divmod(seconds, 3600)
|
|
99
|
+
minutes, seconds = divmod(remainder, 60)
|
|
100
|
+
if hours > 0:
|
|
101
|
+
return f"{int(hours)}h {int(minutes)}m {int(seconds)}s"
|
|
102
|
+
|
|
103
|
+
return f"{int(minutes)}m {int(seconds)}s"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def get_video_properties(video: cv2.VideoCapture, args: argparse.Namespace) -> tuple:
|
|
107
|
+
"""
|
|
108
|
+
Extracts and returns various properties of a video.
|
|
109
|
+
|
|
110
|
+
:param cv2.VideoCapture video: The video capture object.
|
|
111
|
+
:param args: Command line arguments.
|
|
112
|
+
:return: Tuple containing total frames, FPS, video duration in seconds, and video size in bytes.
|
|
113
|
+
"""
|
|
114
|
+
total_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT))
|
|
115
|
+
fps = video.get(cv2.CAP_PROP_FPS)
|
|
116
|
+
video_duration = total_frames / fps if fps > 0 else 0 # in seconds
|
|
117
|
+
video_size = path.getsize(args.input_video_path) # in bytes
|
|
118
|
+
|
|
119
|
+
return total_frames, fps, video_duration, video_size
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def save_barcode_image(barcode: np.ndarray, base_name: str, args: argparse.Namespace, method: str) -> None:
|
|
123
|
+
"""
|
|
124
|
+
Saves a generated barcode image to a file.
|
|
125
|
+
|
|
126
|
+
:param np.ndarray barcode: The barcode image as a NumPy array.
|
|
127
|
+
:param str base_name: The base name of the file to save.
|
|
128
|
+
:param args: Command line arguments.
|
|
129
|
+
:param str method: The method used for color extraction.
|
|
130
|
+
"""
|
|
131
|
+
current_dir = path.dirname(path.abspath(__file__))
|
|
132
|
+
project_root = path.dirname(current_dir) # Go up one directory to get to the project root
|
|
133
|
+
# If destination_path isn't specified, construct one based on the video's name
|
|
134
|
+
if not args.destination_path:
|
|
135
|
+
barcode_dir = path.join(project_root, "barcodes")
|
|
136
|
+
ensure_directory(barcode_dir)
|
|
137
|
+
|
|
138
|
+
# If an output_name is provided by the user
|
|
139
|
+
if args.output_name:
|
|
140
|
+
destination_name = args.output_name + ".png"
|
|
141
|
+
else:
|
|
142
|
+
filename_parts = [base_name, method, args.barcode_type]
|
|
143
|
+
if args.workers:
|
|
144
|
+
filename_parts.append(f"workers_{str(args.workers)}")
|
|
145
|
+
destination_name = "_".join(filename_parts) + ".png"
|
|
146
|
+
|
|
147
|
+
destination_path = path.join(barcode_dir, destination_name)
|
|
148
|
+
else:
|
|
149
|
+
# In case a destination_path is provided, consider appending the method
|
|
150
|
+
# or managing as per your requirement
|
|
151
|
+
destination_path = path.join(project_root, args.destination_path)
|
|
152
|
+
|
|
153
|
+
if barcode.shape[2] == 4: # If the image has an alpha channel (RGBA)
|
|
154
|
+
image = Image.fromarray(barcode, "RGBA")
|
|
155
|
+
else: # If the image doesn't have an alpha channel (RGB)
|
|
156
|
+
image = Image.fromarray(barcode, "RGB")
|
|
157
|
+
|
|
158
|
+
image.save(destination_path)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def ensure_directory(directory_name: str) -> None:
|
|
162
|
+
"""
|
|
163
|
+
Ensures that a directory exists. Creates it if it doesn't.
|
|
164
|
+
|
|
165
|
+
:param str directory_name: The name of the directory to ensure.
|
|
166
|
+
"""
|
|
167
|
+
if not path.exists(directory_name):
|
|
168
|
+
makedirs(directory_name)
|
src/video_processing.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
from multiprocessing import Pool
|
|
2
|
+
from typing import Callable, List, Optional
|
|
3
|
+
|
|
4
|
+
import cv2
|
|
5
|
+
import numpy as np
|
|
6
|
+
from tqdm import tqdm
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def load_video(video_path: str) -> tuple:
|
|
10
|
+
"""
|
|
11
|
+
Load a video file and return its properties.
|
|
12
|
+
|
|
13
|
+
:param str video_path: The path to the video file.
|
|
14
|
+
:return: Tuple containing the video capture object, frame count, frame width, and frame height.
|
|
15
|
+
"""
|
|
16
|
+
video = cv2.VideoCapture(video_path)
|
|
17
|
+
|
|
18
|
+
if not video.isOpened():
|
|
19
|
+
raise ValueError(f"Could not open the video file: {video_path}")
|
|
20
|
+
|
|
21
|
+
frame_count = int(video.get(cv2.CAP_PROP_FRAME_COUNT))
|
|
22
|
+
frame_width = int(video.get(cv2.CAP_PROP_FRAME_WIDTH))
|
|
23
|
+
frame_height = int(video.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
|
24
|
+
|
|
25
|
+
if frame_count <= 0:
|
|
26
|
+
raise ValueError(f"The video file {video_path} has no frames.")
|
|
27
|
+
|
|
28
|
+
if frame_width <= 0 or frame_height <= 0:
|
|
29
|
+
raise ValueError(f"The video file {video_path} has invalid dimensions.")
|
|
30
|
+
|
|
31
|
+
return video, frame_count, frame_width, frame_height
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def parallel_extract_colors(
|
|
35
|
+
video_path: str,
|
|
36
|
+
frame_count: int,
|
|
37
|
+
color_extractor: Callable,
|
|
38
|
+
workers: int,
|
|
39
|
+
target_frames: Optional[int] = None,
|
|
40
|
+
) -> list:
|
|
41
|
+
"""
|
|
42
|
+
Extract dominant colors from frames in a video file using parallel processing.
|
|
43
|
+
|
|
44
|
+
:param str video_path: The path to the video file.
|
|
45
|
+
:param int frame_count: The total number of frames in the video.
|
|
46
|
+
:param Callable color_extractor: A function to extract the dominant color from a frame.
|
|
47
|
+
:param int workers: Number of parallel workers.
|
|
48
|
+
:param Optional[int] target_frames: The total number of frames to sample.
|
|
49
|
+
:return: List of dominant colors for the frames in the video.
|
|
50
|
+
"""
|
|
51
|
+
if target_frames is None:
|
|
52
|
+
target_frames = frame_count
|
|
53
|
+
|
|
54
|
+
frames_per_worker = frame_count // workers
|
|
55
|
+
target_frames_per_worker = target_frames // workers
|
|
56
|
+
|
|
57
|
+
with Pool(workers) as pool:
|
|
58
|
+
args = [
|
|
59
|
+
(
|
|
60
|
+
video_path,
|
|
61
|
+
i * frames_per_worker,
|
|
62
|
+
(i + 1) * frames_per_worker - 1,
|
|
63
|
+
color_extractor,
|
|
64
|
+
target_frames_per_worker,
|
|
65
|
+
)
|
|
66
|
+
for i in range(workers)
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
if frame_count % workers != 0 or target_frames % workers != 0:
|
|
70
|
+
args[-1] = (
|
|
71
|
+
video_path,
|
|
72
|
+
args[-1][1],
|
|
73
|
+
frame_count - 1,
|
|
74
|
+
color_extractor,
|
|
75
|
+
target_frames - (workers - 1) * target_frames_per_worker,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
results = pool.starmap(extract_colors, args)
|
|
79
|
+
|
|
80
|
+
# Concatenate results from all workers
|
|
81
|
+
final_colors = [color for colors in results for color in colors]
|
|
82
|
+
|
|
83
|
+
return final_colors
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def extract_colors(
|
|
87
|
+
video_path: str,
|
|
88
|
+
start_frame: int,
|
|
89
|
+
end_frame: int,
|
|
90
|
+
color_extractor: Callable,
|
|
91
|
+
target_frames: Optional[int] = None,
|
|
92
|
+
) -> List:
|
|
93
|
+
"""
|
|
94
|
+
Extracts dominant colors from frames in a video file.
|
|
95
|
+
|
|
96
|
+
:param str video_path: The video capture object.
|
|
97
|
+
:param int start_frame: The index of the first frame to process.
|
|
98
|
+
:param int end_frame: The index of the last frame to process.
|
|
99
|
+
:param Callable color_extractor: A function to extract the dominant color from a frame.
|
|
100
|
+
:param Optional[int] target_frames: The total number of frames to sample.
|
|
101
|
+
:return: List of dominant colors from the sampled frames.
|
|
102
|
+
"""
|
|
103
|
+
video = cv2.VideoCapture(video_path)
|
|
104
|
+
video.set(cv2.CAP_PROP_POS_FRAMES, start_frame)
|
|
105
|
+
|
|
106
|
+
# Calculate frame_skip based on target_frames
|
|
107
|
+
total_frames = end_frame - start_frame + 1
|
|
108
|
+
if target_frames:
|
|
109
|
+
frame_skip = total_frames // target_frames
|
|
110
|
+
else:
|
|
111
|
+
frame_skip = 1
|
|
112
|
+
|
|
113
|
+
colors = []
|
|
114
|
+
|
|
115
|
+
for _ in tqdm(range(target_frames or total_frames), desc="Processing frames"):
|
|
116
|
+
ret, frame = video.read() # Read the first or next frame
|
|
117
|
+
if ret:
|
|
118
|
+
dominant_color = color_extractor(frame)
|
|
119
|
+
colors.append(dominant_color)
|
|
120
|
+
for _ in range(frame_skip - 1):
|
|
121
|
+
video.grab() # Skip frames
|
|
122
|
+
|
|
123
|
+
video.release()
|
|
124
|
+
|
|
125
|
+
return colors
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def crop_black_borders(frame: np.ndarray, threshold: int = 30) -> np.ndarray:
|
|
129
|
+
"""
|
|
130
|
+
Crop out black borders from a frame.
|
|
131
|
+
|
|
132
|
+
:param np.ndarray frame: Input frame.
|
|
133
|
+
:param int threshold: Threshold below which a pixel is considered 'black'.
|
|
134
|
+
:return np.ndarray: Cropped frame.
|
|
135
|
+
"""
|
|
136
|
+
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
|
137
|
+
|
|
138
|
+
_, binary = cv2.threshold(gray, threshold, 255, cv2.THRESH_BINARY)
|
|
139
|
+
|
|
140
|
+
contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
|
141
|
+
if not contours:
|
|
142
|
+
return frame
|
|
143
|
+
|
|
144
|
+
cnt = max(contours, key=cv2.contourArea)
|
|
145
|
+
x, y, w, h = cv2.boundingRect(cnt)
|
|
146
|
+
|
|
147
|
+
cropped_frame = frame[y : y + h, x : x + w]
|
|
148
|
+
|
|
149
|
+
return cropped_frame
|
tests/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
import numpy as np
|
|
3
|
+
from src import (
|
|
4
|
+
barcode_generation,
|
|
5
|
+
) # Adjust the import as per your project structure and naming
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestBarcodeGeneration(unittest.TestCase):
|
|
9
|
+
"""
|
|
10
|
+
Test the barcode generation functions.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def setUp(self) -> None:
|
|
14
|
+
"""
|
|
15
|
+
Set up the test case.
|
|
16
|
+
:return: None
|
|
17
|
+
"""
|
|
18
|
+
self.colors = [np.array([255, 0, 0]), np.array([0, 255, 0])]
|
|
19
|
+
self.frame_height = 2
|
|
20
|
+
self.frame_count = 2
|
|
21
|
+
self.img_size = 100
|
|
22
|
+
self.width = 1
|
|
23
|
+
|
|
24
|
+
def test_generate_barcode_default(self) -> None:
|
|
25
|
+
"""
|
|
26
|
+
Test the generate_barcode function.
|
|
27
|
+
:return: None
|
|
28
|
+
"""
|
|
29
|
+
barcode = barcode_generation.generate_barcode(self.colors, self.frame_height, self.frame_count)
|
|
30
|
+
|
|
31
|
+
self.assertIsInstance(barcode, np.ndarray)
|
|
32
|
+
self.assertEqual(
|
|
33
|
+
barcode.shape, (self.frame_height, self.frame_count, 3)
|
|
34
|
+
) # Should match the input frame dimensions
|
|
35
|
+
|
|
36
|
+
def test_generate_barcode_with_width(self) -> None:
|
|
37
|
+
"""
|
|
38
|
+
Test the generate_barcode function with a specified width.
|
|
39
|
+
:return: None
|
|
40
|
+
"""
|
|
41
|
+
barcode = barcode_generation.generate_barcode(self.colors, self.frame_height, self.frame_count, self.width)
|
|
42
|
+
|
|
43
|
+
self.assertIsInstance(barcode, np.ndarray)
|
|
44
|
+
self.assertEqual(barcode.shape, (self.frame_height, self.width, 3))
|
|
45
|
+
|
|
46
|
+
def test_generate_circular_barcode(self) -> None:
|
|
47
|
+
"""
|
|
48
|
+
Test the generate_circular_barcode function.
|
|
49
|
+
:return: None
|
|
50
|
+
"""
|
|
51
|
+
barcode = barcode_generation.generate_circular_barcode(self.colors, self.img_size)
|
|
52
|
+
|
|
53
|
+
self.assertIsInstance(barcode, np.ndarray)
|
|
54
|
+
self.assertEqual(barcode.shape, (self.img_size, self.img_size, 4)) # RGBA image
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if __name__ == "__main__":
|
|
58
|
+
unittest.main()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
import numpy as np
|
|
3
|
+
from src import (
|
|
4
|
+
color_extraction,
|
|
5
|
+
) # Adjust the import based on your project structure and naming
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestColorExtraction(unittest.TestCase):
|
|
9
|
+
"""
|
|
10
|
+
Test the color extraction functions.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def setUp(self) -> None:
|
|
14
|
+
self.frame = np.array([[[255, 0, 0], [0, 255, 0]], [[0, 0, 255], [255, 255, 255]]], dtype=np.uint8)
|
|
15
|
+
|
|
16
|
+
def test_get_dominant_color_mean(self) -> None:
|
|
17
|
+
"""
|
|
18
|
+
Test the get_dominant_color_mean function.
|
|
19
|
+
:return: None
|
|
20
|
+
"""
|
|
21
|
+
# Call your function with the sample frame
|
|
22
|
+
dominant_color = color_extraction.get_dominant_color_mean(self.frame)
|
|
23
|
+
|
|
24
|
+
# Assert the expected result
|
|
25
|
+
self.assertEqual(dominant_color.tolist(), [127.5, 127.5, 127.5])
|
|
26
|
+
|
|
27
|
+
def test_get_dominant_color_kmeans(self) -> None:
|
|
28
|
+
"""
|
|
29
|
+
Test the get_dominant_color_kmeans function.
|
|
30
|
+
:return: None
|
|
31
|
+
"""
|
|
32
|
+
dominant_color = color_extraction.get_dominant_color_kmeans(self.frame)
|
|
33
|
+
|
|
34
|
+
self.assertIsInstance(dominant_color, np.ndarray)
|
|
35
|
+
self.assertEqual(dominant_color.shape, (3,)) # Should be a 3-element array representing a color
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
if __name__ == "__main__":
|
|
39
|
+
unittest.main()
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import unittest
|
|
3
|
+
import os
|
|
4
|
+
import glob
|
|
5
|
+
|
|
6
|
+
from src import main
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestIntegration(unittest.TestCase):
|
|
10
|
+
def setUp(self) -> None:
|
|
11
|
+
"""
|
|
12
|
+
Set up the test case.
|
|
13
|
+
"""
|
|
14
|
+
# Recreate the parser with minimal necessary setup
|
|
15
|
+
self.parser = argparse.ArgumentParser(description="Test Parser")
|
|
16
|
+
self.parser.add_argument("--input_video_path", type=str)
|
|
17
|
+
self.parser.add_argument("--destination_path", type=str, nargs="?", default=None)
|
|
18
|
+
self.parser.add_argument("--barcode_type", choices=["horizontal", "circular"], default="horizontal")
|
|
19
|
+
self.parser.add_argument("--method", choices=["avg", "kmeans", "hsv", "bgr"], default="avg")
|
|
20
|
+
self.parser.add_argument("--workers", type=int, default=None)
|
|
21
|
+
self.parser.add_argument("--width", type=int, default=None)
|
|
22
|
+
self.parser.add_argument("--output_name", type=str, nargs="?", default=None)
|
|
23
|
+
self.parser.add_argument("--all_methods", type=bool, default=False)
|
|
24
|
+
|
|
25
|
+
self.input_video_path = "tests/sample.mp4"
|
|
26
|
+
|
|
27
|
+
def _run_test(self, barcode_type, workers, width=None):
|
|
28
|
+
args = [
|
|
29
|
+
"--input_video_path",
|
|
30
|
+
self.input_video_path,
|
|
31
|
+
"--barcode_type",
|
|
32
|
+
barcode_type,
|
|
33
|
+
"--method",
|
|
34
|
+
"avg",
|
|
35
|
+
"--workers",
|
|
36
|
+
str(workers),
|
|
37
|
+
]
|
|
38
|
+
if width is not None:
|
|
39
|
+
args.extend(["--width", str(width)])
|
|
40
|
+
|
|
41
|
+
# Parse args using the recreated parser
|
|
42
|
+
parsed_args = self.parser.parse_args(args)
|
|
43
|
+
|
|
44
|
+
# Execute the program with the parsed Namespace object
|
|
45
|
+
main.main(parsed_args)
|
|
46
|
+
|
|
47
|
+
def test_horizontal_1_worker_default_width(self):
|
|
48
|
+
self._run_test("horizontal", 1)
|
|
49
|
+
|
|
50
|
+
def test_horizontal_1_worker_defined_width(self):
|
|
51
|
+
self._run_test("horizontal", 1, 90)
|
|
52
|
+
|
|
53
|
+
def test_horizontal_2_workers_default_width(self):
|
|
54
|
+
self._run_test("horizontal", 2)
|
|
55
|
+
|
|
56
|
+
def test_horizontal_2_workers_defined_width(self):
|
|
57
|
+
self._run_test("horizontal", 2, 90)
|
|
58
|
+
|
|
59
|
+
def test_circular_1_worker_default_width(self):
|
|
60
|
+
self._run_test("circular", 1)
|
|
61
|
+
|
|
62
|
+
def test_circular_1_worker_defined_width(self):
|
|
63
|
+
self._run_test("circular", 1, 90)
|
|
64
|
+
|
|
65
|
+
def test_circular_2_workers_default_width(self):
|
|
66
|
+
self._run_test("circular", 2)
|
|
67
|
+
|
|
68
|
+
def test_circular_2_workers_defined_width(self):
|
|
69
|
+
self._run_test("circular", 2, 90)
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def tearDownClass(cls) -> None:
|
|
73
|
+
"""
|
|
74
|
+
Clean up by deleting the generated images that start with "sample_"
|
|
75
|
+
after all tests in this class have run.
|
|
76
|
+
"""
|
|
77
|
+
# Specify the path to the barcode images that start with "sample_"
|
|
78
|
+
barcode_images_path = "barcodes/sample_*"
|
|
79
|
+
# Use glob.glob to find all matching files
|
|
80
|
+
for img_file in glob.glob(barcode_images_path):
|
|
81
|
+
try:
|
|
82
|
+
os.remove(img_file) # Remove the file
|
|
83
|
+
except OSError as e:
|
|
84
|
+
print(f"Error: {img_file} : {e.strerror}")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
if __name__ == "__main__":
|
|
88
|
+
unittest.main()
|