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.

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)
@@ -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()