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
tests/test_utility.py
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
import argparse
|
|
3
|
+
from unittest.mock import patch, MagicMock, Mock
|
|
4
|
+
import cv2
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
from src import utility
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestUtility(unittest.TestCase):
|
|
12
|
+
"""
|
|
13
|
+
Test the utility functions.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def setUp(self) -> None:
|
|
17
|
+
"""
|
|
18
|
+
Set up the test case.
|
|
19
|
+
:return: None
|
|
20
|
+
"""
|
|
21
|
+
super().setUp()
|
|
22
|
+
self.args = argparse.Namespace(
|
|
23
|
+
input_video_path="test_video.mp4",
|
|
24
|
+
destination_path="/test/output.png",
|
|
25
|
+
workers=4,
|
|
26
|
+
width=200,
|
|
27
|
+
all_methods=False,
|
|
28
|
+
method="avg",
|
|
29
|
+
)
|
|
30
|
+
self.frame_count = 300
|
|
31
|
+
self.MAX_PROCESSES = 8
|
|
32
|
+
self.MIN_FRAME_COUNT = 100
|
|
33
|
+
# Mock setup for path.exists and access to ensure they return True by default
|
|
34
|
+
patcher_exists = patch("src.utility.path.exists", return_value=True)
|
|
35
|
+
patcher_access = patch("src.utility.access", return_value=True)
|
|
36
|
+
self.addCleanup(patcher_exists.stop)
|
|
37
|
+
self.addCleanup(patcher_access.stop)
|
|
38
|
+
self.mock_exists = patcher_exists.start()
|
|
39
|
+
self.mock_access = patcher_access.start()
|
|
40
|
+
|
|
41
|
+
@patch("src.utility.path.exists")
|
|
42
|
+
def test_file_not_found_error(self, mock_exists: MagicMock) -> None:
|
|
43
|
+
"""
|
|
44
|
+
Test that validate_args raises a FileNotFoundError when the input video file does not exist.
|
|
45
|
+
:param mock_exists: MagicMock object for path.exists function to return False
|
|
46
|
+
:return: None
|
|
47
|
+
"""
|
|
48
|
+
mock_exists.return_value = False
|
|
49
|
+
with self.assertRaises(FileNotFoundError):
|
|
50
|
+
utility.validate_args(self.args, self.frame_count, self.MAX_PROCESSES, self.MIN_FRAME_COUNT)
|
|
51
|
+
|
|
52
|
+
@patch("src.utility.path.exists")
|
|
53
|
+
def test_invalid_extension_error(self, mock_exists: MagicMock) -> None:
|
|
54
|
+
"""
|
|
55
|
+
Test that validate_args raises a ValueError when the input video file has an invalid extension.
|
|
56
|
+
:param mock_exists: MagicMock object for path.exists function to return True
|
|
57
|
+
:return: None
|
|
58
|
+
"""
|
|
59
|
+
mock_exists.return_value = True
|
|
60
|
+
self.args.input_video_path = "invalid_extension.txt"
|
|
61
|
+
with self.assertRaises(ValueError):
|
|
62
|
+
utility.validate_args(self.args, self.frame_count, self.MAX_PROCESSES, self.MIN_FRAME_COUNT)
|
|
63
|
+
|
|
64
|
+
@patch("src.utility.path.exists")
|
|
65
|
+
@patch("src.utility.access")
|
|
66
|
+
def test_destination_path_not_writable(self, mock_access: MagicMock, mock_exists: MagicMock) -> None:
|
|
67
|
+
"""
|
|
68
|
+
Test that validate_args raises a PermissionError when the destination path is not writable.
|
|
69
|
+
:param mock_access: MagicMock object for os.access function to return False
|
|
70
|
+
:param mock_exists: MagicMock object for path.exists function to return True
|
|
71
|
+
:return: None
|
|
72
|
+
"""
|
|
73
|
+
mock_exists.return_value = True
|
|
74
|
+
mock_access.return_value = False
|
|
75
|
+
with self.assertRaises(PermissionError):
|
|
76
|
+
utility.validate_args(self.args, self.frame_count, self.MAX_PROCESSES, self.MIN_FRAME_COUNT)
|
|
77
|
+
|
|
78
|
+
@patch("src.utility.path.exists")
|
|
79
|
+
def test_workers_value_error(self, mock_exists: MagicMock) -> None:
|
|
80
|
+
"""
|
|
81
|
+
Test that validate_args raises a ValueError when the number of workers is invalid.
|
|
82
|
+
:param mock_exists: MagicMock object for path.exists function to return True
|
|
83
|
+
:return: None
|
|
84
|
+
"""
|
|
85
|
+
mock_exists.return_value = True
|
|
86
|
+
self.args.workers = 0 # Testing for < 1
|
|
87
|
+
with self.assertRaises(ValueError):
|
|
88
|
+
utility.validate_args(self.args, self.frame_count, self.MAX_PROCESSES, self.MIN_FRAME_COUNT)
|
|
89
|
+
|
|
90
|
+
self.args.workers = self.MAX_PROCESSES + 1 # Testing for > MAX_PROCESSES
|
|
91
|
+
with self.assertRaises(ValueError):
|
|
92
|
+
utility.validate_args(self.args, self.frame_count, self.MAX_PROCESSES, self.MIN_FRAME_COUNT)
|
|
93
|
+
|
|
94
|
+
def test_invalid_width(self) -> None:
|
|
95
|
+
"""
|
|
96
|
+
Test that validate_args raises a ValueError when the width is invalid.
|
|
97
|
+
:return: None
|
|
98
|
+
"""
|
|
99
|
+
self.args.width = 0 # Testing for <= 0
|
|
100
|
+
with self.assertRaises(ValueError):
|
|
101
|
+
utility.validate_args(self.args, self.frame_count, self.MAX_PROCESSES, self.MIN_FRAME_COUNT)
|
|
102
|
+
|
|
103
|
+
self.args.width = self.frame_count + 1 # Testing for > frame_count
|
|
104
|
+
with self.assertRaises(ValueError):
|
|
105
|
+
utility.validate_args(self.args, self.frame_count, self.MAX_PROCESSES, self.MIN_FRAME_COUNT)
|
|
106
|
+
|
|
107
|
+
def test_frame_count_too_low(self) -> None:
|
|
108
|
+
"""
|
|
109
|
+
Test that validate_args raises a ValueError when the frame count is too low.
|
|
110
|
+
:return: None
|
|
111
|
+
"""
|
|
112
|
+
low_frame_count = self.MIN_FRAME_COUNT - 1
|
|
113
|
+
with self.assertRaises(ValueError):
|
|
114
|
+
utility.validate_args(self.args, low_frame_count, self.MAX_PROCESSES, self.MIN_FRAME_COUNT)
|
|
115
|
+
|
|
116
|
+
def test_all_methods_and_method_error(self) -> None:
|
|
117
|
+
"""
|
|
118
|
+
Test that validate_args raises a ValueError when the --all_methods flag is used with the --method argument.
|
|
119
|
+
:return: None
|
|
120
|
+
"""
|
|
121
|
+
self.args.all_methods = True
|
|
122
|
+
self.args.method = "avg" # Explicitly setting method to simulate conflict
|
|
123
|
+
with self.assertRaises(ValueError):
|
|
124
|
+
utility.validate_args(self.args, self.frame_count, self.MAX_PROCESSES, self.MIN_FRAME_COUNT)
|
|
125
|
+
|
|
126
|
+
def test_no_error_raised(self) -> None:
|
|
127
|
+
"""
|
|
128
|
+
Test that no error is raised when all arguments are valid.
|
|
129
|
+
:return: None
|
|
130
|
+
"""
|
|
131
|
+
utility.validate_args(self.args, self.frame_count, self.MAX_PROCESSES, self.MIN_FRAME_COUNT)
|
|
132
|
+
|
|
133
|
+
@patch("src.utility.makedirs")
|
|
134
|
+
@patch("src.utility.path.exists")
|
|
135
|
+
def test_ensure_directory_creates_directory(self, mock_exists: MagicMock, mock_makedirs: MagicMock) -> None:
|
|
136
|
+
"""
|
|
137
|
+
Test ensure_directory creates the directory when it does not exist.
|
|
138
|
+
:param mock_exists: MagicMock object for path.exists function to return False
|
|
139
|
+
:param mock_makedirs: MagicMock object for os.makedirs function
|
|
140
|
+
:return: None
|
|
141
|
+
"""
|
|
142
|
+
mock_exists.return_value = False # Simulate directory does not exist
|
|
143
|
+
|
|
144
|
+
utility.ensure_directory(self.args.destination_path)
|
|
145
|
+
|
|
146
|
+
mock_exists.assert_called_once_with(self.args.destination_path)
|
|
147
|
+
mock_makedirs.assert_called_once_with(self.args.destination_path)
|
|
148
|
+
|
|
149
|
+
def test_format_time_seconds_only(self) -> None:
|
|
150
|
+
"""
|
|
151
|
+
Test that format_time correctly formats times less than 60 seconds.
|
|
152
|
+
:return: None
|
|
153
|
+
"""
|
|
154
|
+
time_seconds = 45.0 # 45 seconds
|
|
155
|
+
expected_format = "0m 45s"
|
|
156
|
+
self.assertEqual(utility.format_time(time_seconds), expected_format)
|
|
157
|
+
|
|
158
|
+
def test_format_time_minutes_and_seconds(self) -> None:
|
|
159
|
+
"""
|
|
160
|
+
Test that format_time correctly formats times between 1 minute and less than 1 hour.
|
|
161
|
+
:return: None
|
|
162
|
+
"""
|
|
163
|
+
time_seconds = 95.0 # 1 minute and 35 seconds
|
|
164
|
+
expected_format = "1m 35s"
|
|
165
|
+
self.assertEqual(utility.format_time(time_seconds), expected_format)
|
|
166
|
+
|
|
167
|
+
def test_format_time_hours_minutes_seconds(self) -> None:
|
|
168
|
+
"""
|
|
169
|
+
Test that format_time correctly formats times with hours, minutes, and seconds.
|
|
170
|
+
:return: None
|
|
171
|
+
"""
|
|
172
|
+
time_seconds = 3725.0 # 1 hour, 2 minutes, and 5 seconds
|
|
173
|
+
expected_format = "1h 2m 5s"
|
|
174
|
+
self.assertEqual(utility.format_time(time_seconds), expected_format)
|
|
175
|
+
|
|
176
|
+
def test_format_time_rounding(self) -> None:
|
|
177
|
+
"""
|
|
178
|
+
Test that format_time correctly rounds seconds.
|
|
179
|
+
:return: None
|
|
180
|
+
"""
|
|
181
|
+
time_seconds = 3661.6 # Should round to 1 hour, 1 minute, and 1 seconds
|
|
182
|
+
expected_format = "1h 1m 1s"
|
|
183
|
+
self.assertEqual(utility.format_time(time_seconds), expected_format)
|
|
184
|
+
|
|
185
|
+
def test_get_dominant_color_function_with_invalid_method(self) -> None:
|
|
186
|
+
"""
|
|
187
|
+
Test that get_dominant_color_function raises ValueError for unsupported methods.
|
|
188
|
+
:return: None
|
|
189
|
+
"""
|
|
190
|
+
with self.assertRaises(ValueError):
|
|
191
|
+
utility.get_dominant_color_function("unsupported_method")
|
|
192
|
+
|
|
193
|
+
def test_get_dominant_color_function_with_valid_methods(self) -> None:
|
|
194
|
+
"""
|
|
195
|
+
Test that get_dominant_color_function returns a callable for supported methods.
|
|
196
|
+
:return: None
|
|
197
|
+
"""
|
|
198
|
+
valid_methods = ["avg", "kmeans", "hsv", "bgr"]
|
|
199
|
+
for method in valid_methods:
|
|
200
|
+
result = utility.get_dominant_color_function(method)
|
|
201
|
+
self.assertTrue(callable(result), f"Method '{method}' should return a callable.")
|
|
202
|
+
|
|
203
|
+
def test_get_dominant_color_function_returns_specific_function(self) -> None:
|
|
204
|
+
"""
|
|
205
|
+
Test that get_dominant_color_function returns the specific function associated with a method.
|
|
206
|
+
:return: None
|
|
207
|
+
"""
|
|
208
|
+
# Mocking the specific functions to test if the correct one is returned
|
|
209
|
+
utility.get_dominant_color_mean = Mock(name="get_dominant_color_mean")
|
|
210
|
+
utility.get_dominant_color_kmeans = Mock(name="get_dominant_color_kmeans")
|
|
211
|
+
utility.get_dominant_color_hsv = Mock(name="get_dominant_color_hsv")
|
|
212
|
+
utility.get_dominant_color_bgr = Mock(name="get_dominant_color_bgr")
|
|
213
|
+
|
|
214
|
+
self.assertEqual(utility.get_dominant_color_function("avg"), utility.get_dominant_color_mean)
|
|
215
|
+
self.assertEqual(
|
|
216
|
+
utility.get_dominant_color_function("kmeans"),
|
|
217
|
+
utility.get_dominant_color_kmeans,
|
|
218
|
+
)
|
|
219
|
+
self.assertEqual(utility.get_dominant_color_function("hsv"), utility.get_dominant_color_hsv)
|
|
220
|
+
self.assertEqual(utility.get_dominant_color_function("bgr"), utility.get_dominant_color_bgr)
|
|
221
|
+
|
|
222
|
+
@patch("src.utility.path.getsize")
|
|
223
|
+
def test_get_video_properties(self, mock_getsize: MagicMock) -> None:
|
|
224
|
+
"""
|
|
225
|
+
Test that get_video_properties correctly extracts video properties.
|
|
226
|
+
:param mock_getsize: MagicMock object for path.getsize function
|
|
227
|
+
:return: None
|
|
228
|
+
"""
|
|
229
|
+
# Setup mock return values
|
|
230
|
+
total_frames = 100
|
|
231
|
+
fps = 25
|
|
232
|
+
video_duration = total_frames / fps
|
|
233
|
+
video_size = 1024000 # in bytes
|
|
234
|
+
|
|
235
|
+
# Configure the mock objects
|
|
236
|
+
mock_video = MagicMock()
|
|
237
|
+
mock_video.get.side_effect = [
|
|
238
|
+
total_frames,
|
|
239
|
+
fps,
|
|
240
|
+
] # CAP_PROP_FRAME_COUNT, CAP_PROP_FPS
|
|
241
|
+
mock_getsize.return_value = video_size
|
|
242
|
+
|
|
243
|
+
args = argparse.Namespace(input_video_path="test/sample.mp4")
|
|
244
|
+
|
|
245
|
+
# Call the function with the mocked objects/return values
|
|
246
|
+
result = utility.get_video_properties(mock_video, args)
|
|
247
|
+
|
|
248
|
+
# Assert that the function returns the expected tuple
|
|
249
|
+
expected_result = (total_frames, fps, video_duration, video_size)
|
|
250
|
+
self.assertEqual(result, expected_result)
|
|
251
|
+
|
|
252
|
+
# Verify that the mocks were called as expected
|
|
253
|
+
mock_video.get.assert_any_call(cv2.CAP_PROP_FRAME_COUNT)
|
|
254
|
+
mock_video.get.assert_any_call(cv2.CAP_PROP_FPS)
|
|
255
|
+
mock_getsize.assert_called_once_with(args.input_video_path)
|
|
256
|
+
|
|
257
|
+
@patch("src.utility.path.join")
|
|
258
|
+
@patch("src.utility.Image.fromarray")
|
|
259
|
+
@patch("src.utility.path.dirname")
|
|
260
|
+
@patch("src.utility.path.abspath")
|
|
261
|
+
def test_save_barcode_image_variations(
|
|
262
|
+
self,
|
|
263
|
+
mock_abspath: MagicMock,
|
|
264
|
+
mock_dirname: MagicMock,
|
|
265
|
+
mock_fromarray: MagicMock,
|
|
266
|
+
mock_path_join: MagicMock,
|
|
267
|
+
) -> None:
|
|
268
|
+
"""
|
|
269
|
+
Test variations of save_barcode_image behavior based on different argument conditions.
|
|
270
|
+
:param mock_abspath: MagicMock object for path.abspath function
|
|
271
|
+
:param mock_dirname: MagicMock object for path.dirname function
|
|
272
|
+
:param mock_fromarray: MagicMock object for Image.fromarray function
|
|
273
|
+
:param mock_path_join: MagicMock object for path.join function
|
|
274
|
+
:return: None
|
|
275
|
+
"""
|
|
276
|
+
# Mock the directory and path handling
|
|
277
|
+
mock_abspath.return_value = "/fake/dir/module.py"
|
|
278
|
+
mock_dirname.side_effect = lambda x: x.rsplit("/", 1)[0] # Simulates dirname behavior
|
|
279
|
+
mock_path_join.side_effect = lambda *args: "/".join(args) # Simulates os.path.join behavior
|
|
280
|
+
|
|
281
|
+
# Setup a fake barcode and base name
|
|
282
|
+
barcode = np.zeros((100, 100, 3), dtype=np.uint8)
|
|
283
|
+
base_name = "video_sample"
|
|
284
|
+
|
|
285
|
+
# Test without workers and without output name
|
|
286
|
+
args_without_workers = argparse.Namespace(
|
|
287
|
+
destination_path=None, output_name=None, barcode_type="type1", workers=None
|
|
288
|
+
)
|
|
289
|
+
utility.save_barcode_image(barcode, base_name, args_without_workers, "avg")
|
|
290
|
+
self.assertTrue("workers_" not in mock_path_join.call_args[0][-1])
|
|
291
|
+
|
|
292
|
+
# Test with workers
|
|
293
|
+
args_with_workers = argparse.Namespace(destination_path=None, output_name=None, barcode_type="type1", workers=4)
|
|
294
|
+
utility.save_barcode_image(barcode, base_name, args_with_workers, "avg")
|
|
295
|
+
self.assertTrue("workers_4" in mock_path_join.call_args[0][-1])
|
|
296
|
+
|
|
297
|
+
# Test with output name
|
|
298
|
+
args_with_output_name = argparse.Namespace(
|
|
299
|
+
destination_path=None,
|
|
300
|
+
output_name="custom_name",
|
|
301
|
+
barcode_type="type1",
|
|
302
|
+
workers=None,
|
|
303
|
+
)
|
|
304
|
+
utility.save_barcode_image(barcode, base_name, args_with_output_name, "avg")
|
|
305
|
+
self.assertIn("custom_name.png", mock_path_join.call_args[0][-1])
|
|
306
|
+
|
|
307
|
+
# Test file naming without output name
|
|
308
|
+
utility.save_barcode_image(barcode, base_name, args_without_workers, "avg")
|
|
309
|
+
expected_name_parts = [base_name, "avg", "type1"]
|
|
310
|
+
expected_name = "_".join(expected_name_parts) + ".png"
|
|
311
|
+
self.assertIn(expected_name, mock_path_join.call_args[0][-1])
|
|
312
|
+
|
|
313
|
+
# Test image saving
|
|
314
|
+
mock_image = mock_fromarray.return_value
|
|
315
|
+
utility.save_barcode_image(barcode, base_name, args_without_workers, "avg")
|
|
316
|
+
mock_image.save.assert_called() # Ensure the image is attempted to be saved
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
if __name__ == "__main__":
|
|
320
|
+
unittest.main()
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from unittest.mock import patch, MagicMock
|
|
3
|
+
from src import video_processing
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestVideoProcessing(unittest.TestCase):
|
|
7
|
+
"""
|
|
8
|
+
Test the video processing functions.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
def setUp(self) -> None:
|
|
12
|
+
"""
|
|
13
|
+
Set up the test case.
|
|
14
|
+
:return: None
|
|
15
|
+
"""
|
|
16
|
+
self.video_path = "dummy_video.mp4"
|
|
17
|
+
self.start_frame = 0
|
|
18
|
+
self.end_frame = 49
|
|
19
|
+
self.target_frames = 5
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def mock_color_extractor(frame):
|
|
23
|
+
"""
|
|
24
|
+
Mock color extractor function.
|
|
25
|
+
:param frame:
|
|
26
|
+
:return: frame
|
|
27
|
+
"""
|
|
28
|
+
return frame
|
|
29
|
+
|
|
30
|
+
@patch("src.video_processing.cv2.VideoCapture")
|
|
31
|
+
def test_load_video_raises_error_on_file_not_open(self, mock_video: MagicMock) -> None:
|
|
32
|
+
"""
|
|
33
|
+
Test load_video raises ValueError when the video file cannot be opened.
|
|
34
|
+
:param mock_video: MagicMock object for cv2.VideoCapture
|
|
35
|
+
:return: None
|
|
36
|
+
"""
|
|
37
|
+
mock_video.return_value.isOpened.return_value = False
|
|
38
|
+
with self.assertRaises(ValueError) as context:
|
|
39
|
+
video_processing.load_video(self.video_path)
|
|
40
|
+
self.assertIn(
|
|
41
|
+
"Could not open the video file: " + self.video_path,
|
|
42
|
+
str(context.exception),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
@patch("src.video_processing.cv2.VideoCapture")
|
|
46
|
+
def test_load_video_raises_error_on_no_frames(self, mock_video: MagicMock) -> None:
|
|
47
|
+
"""
|
|
48
|
+
Test load_video raises ValueError when the video has no frames.
|
|
49
|
+
:param mock_video: MagicMock object for cv2.VideoCapture
|
|
50
|
+
:return: None
|
|
51
|
+
"""
|
|
52
|
+
mock_video.return_value.isOpened.return_value = True
|
|
53
|
+
mock_video.return_value.get.return_value = 0 # Mocking frame count as 0
|
|
54
|
+
with self.assertRaises(ValueError) as context:
|
|
55
|
+
video_processing.load_video(self.video_path)
|
|
56
|
+
self.assertIn(
|
|
57
|
+
"The video file " + self.video_path + " has no frames.",
|
|
58
|
+
str(context.exception),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
@patch("src.video_processing.cv2.VideoCapture")
|
|
62
|
+
def test_load_video_raises_error_on_invalid_dimensions(self, mock_video: MagicMock) -> None:
|
|
63
|
+
"""
|
|
64
|
+
Test load_video raises ValueError when video has invalid dimensions.
|
|
65
|
+
:param mock_video: MagicMock object for cv2.VideoCapture
|
|
66
|
+
:return: None
|
|
67
|
+
"""
|
|
68
|
+
mock_video.return_value.isOpened.return_value = True
|
|
69
|
+
mock_video.return_value.get.side_effect = [
|
|
70
|
+
1,
|
|
71
|
+
0,
|
|
72
|
+
0,
|
|
73
|
+
] # Mocking frame count as 1 and dimensions as 0
|
|
74
|
+
with self.assertRaises(ValueError) as context:
|
|
75
|
+
video_processing.load_video(self.video_path)
|
|
76
|
+
self.assertIn(
|
|
77
|
+
"The video file " + self.video_path + " has invalid dimensions.",
|
|
78
|
+
str(context.exception),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
@patch("src.video_processing.cv2.VideoCapture")
|
|
82
|
+
def test_load_video_success(self, mock_video: MagicMock) -> None:
|
|
83
|
+
"""
|
|
84
|
+
Test load_video successfully returns video properties.
|
|
85
|
+
:param mock_video: MagicMock object for cv2.VideoCapture
|
|
86
|
+
:return: None
|
|
87
|
+
"""
|
|
88
|
+
mock_video.return_value.isOpened.return_value = True
|
|
89
|
+
mock_video.return_value.get.side_effect = [
|
|
90
|
+
100,
|
|
91
|
+
1920,
|
|
92
|
+
1080,
|
|
93
|
+
] # Mocking frame count, width, and height
|
|
94
|
+
video, frame_count, frame_width, frame_height = video_processing.load_video(self.video_path)
|
|
95
|
+
self.assertEqual(frame_count, 100)
|
|
96
|
+
self.assertEqual(frame_width, 1920)
|
|
97
|
+
self.assertEqual(frame_height, 1080)
|
|
98
|
+
|
|
99
|
+
@patch("cv2.VideoCapture")
|
|
100
|
+
def test_extract_colors(self, mock_video: MagicMock) -> None:
|
|
101
|
+
"""
|
|
102
|
+
Test extract_colors returns the expected colors.
|
|
103
|
+
:param mock_video: MagicMock object for cv2.VideoCapture
|
|
104
|
+
:return: None
|
|
105
|
+
"""
|
|
106
|
+
# Mock the read method of VideoCapture
|
|
107
|
+
mock_video_instance = mock_video.return_value
|
|
108
|
+
mock_video_instance.read.side_effect = [(True, "frame_data")] * self.target_frames # Simulate frames
|
|
109
|
+
|
|
110
|
+
# Mock color_extractor to return a predictable color
|
|
111
|
+
def special_color_extractor(frame):
|
|
112
|
+
return "red" if frame == "frame_data" else "unknown"
|
|
113
|
+
|
|
114
|
+
# Expected result
|
|
115
|
+
expected_colors = ["red"] * self.target_frames
|
|
116
|
+
|
|
117
|
+
# Run the test
|
|
118
|
+
actual_colors = video_processing.extract_colors(
|
|
119
|
+
self.video_path,
|
|
120
|
+
self.start_frame,
|
|
121
|
+
self.end_frame,
|
|
122
|
+
special_color_extractor,
|
|
123
|
+
self.target_frames,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Verify the result
|
|
127
|
+
self.assertEqual(actual_colors, expected_colors)
|
|
128
|
+
|
|
129
|
+
@patch("cv2.VideoCapture")
|
|
130
|
+
def test_frame_skipping_logic(self, mock_video: MagicMock) -> None:
|
|
131
|
+
"""
|
|
132
|
+
Test that the frame skipping logic is working as expected.
|
|
133
|
+
:param mock_video: MagicMock object for cv2.VideoCapture
|
|
134
|
+
:return: None
|
|
135
|
+
"""
|
|
136
|
+
mock_video_instance = mock_video.return_value
|
|
137
|
+
|
|
138
|
+
# Initialize a counter to keep track of the current frame position
|
|
139
|
+
current_frame = [0] # Use a list so it can be modified within the side_effect functions
|
|
140
|
+
|
|
141
|
+
# Define a side effect for the read method
|
|
142
|
+
def read_side_effect():
|
|
143
|
+
if current_frame[0] < self.end_frame:
|
|
144
|
+
frame_data = (True, f"frame_data_{current_frame[0]}")
|
|
145
|
+
current_frame[0] += 1
|
|
146
|
+
return frame_data
|
|
147
|
+
else:
|
|
148
|
+
return False, None # Simulate end of video
|
|
149
|
+
|
|
150
|
+
# Define a side effect for the grab method to simply increment the counter
|
|
151
|
+
def grab_side_effect():
|
|
152
|
+
if current_frame[0] < self.end_frame:
|
|
153
|
+
current_frame[0] += 1
|
|
154
|
+
|
|
155
|
+
mock_video_instance.read.side_effect = read_side_effect
|
|
156
|
+
mock_video_instance.grab.side_effect = grab_side_effect
|
|
157
|
+
|
|
158
|
+
expected_colors = [f"frame_data_{i*10}" for i in range(self.target_frames)]
|
|
159
|
+
|
|
160
|
+
actual_colors = video_processing.extract_colors(
|
|
161
|
+
self.video_path,
|
|
162
|
+
self.start_frame,
|
|
163
|
+
self.end_frame,
|
|
164
|
+
self.mock_color_extractor,
|
|
165
|
+
self.target_frames,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
self.assertEqual(actual_colors, expected_colors)
|
|
169
|
+
|
|
170
|
+
@patch("cv2.VideoCapture")
|
|
171
|
+
def test_frame_skip_default(self, mock_video: MagicMock) -> None:
|
|
172
|
+
"""
|
|
173
|
+
Test that the default frame_skip is 1.
|
|
174
|
+
:param mock_video: MagicMock object for cv2.VideoCapture
|
|
175
|
+
:return: None
|
|
176
|
+
"""
|
|
177
|
+
# Setup mock to simulate frames in the video
|
|
178
|
+
mock_video_instance = mock_video.return_value
|
|
179
|
+
mock_video_instance.read.side_effect = [(True, f"frame_{i}") for i in range(self.end_frame)] + [(False, None)]
|
|
180
|
+
|
|
181
|
+
# Call without specifying target_frames
|
|
182
|
+
colors = video_processing.extract_colors(
|
|
183
|
+
self.video_path, self.start_frame, self.end_frame, self.mock_color_extractor
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Assert frame_skip was effectively 1 by checking all frames were processed
|
|
187
|
+
self.assertEqual(len(colors), self.end_frame)
|
|
188
|
+
|
|
189
|
+
@patch("cv2.VideoCapture")
|
|
190
|
+
def test_frame_skip_with_target_frames(self, mock_video: MagicMock) -> None:
|
|
191
|
+
"""
|
|
192
|
+
Test that target_frames is respected when extracting colors.
|
|
193
|
+
:param mock_video: MagicMock object for cv2.VideoCapture
|
|
194
|
+
:return: None
|
|
195
|
+
"""
|
|
196
|
+
# Setup mock to simulate frames in the video
|
|
197
|
+
mock_video_instance = mock_video.return_value
|
|
198
|
+
mock_video_instance.read.side_effect = [(True, f"frame_{i}") for i in range(self.end_frame)] + [(False, None)]
|
|
199
|
+
|
|
200
|
+
colors = video_processing.extract_colors(
|
|
201
|
+
self.video_path,
|
|
202
|
+
self.start_frame,
|
|
203
|
+
self.end_frame,
|
|
204
|
+
self.mock_color_extractor,
|
|
205
|
+
self.target_frames,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Assert that we only processed the specified number of target_frames
|
|
209
|
+
self.assertEqual(len(colors), self.target_frames)
|
|
210
|
+
|
|
211
|
+
@patch("cv2.VideoCapture")
|
|
212
|
+
def test_len_colors_equals_len_video_no_target_frames(self, mock_video: MagicMock) -> None:
|
|
213
|
+
"""
|
|
214
|
+
Test that the number of colors extracted is equal to the number of frames in the video
|
|
215
|
+
when target_frames is not given.
|
|
216
|
+
:param mock_video: MagicMock object for cv2.VideoCapture
|
|
217
|
+
:return: None
|
|
218
|
+
"""
|
|
219
|
+
# Setup mock to simulate frames in the video
|
|
220
|
+
mock_video_instance = mock_video.return_value
|
|
221
|
+
mock_video_instance.read.side_effect = [(True, f"frame_{i}") for i in range(self.end_frame)] + [(False, None)]
|
|
222
|
+
|
|
223
|
+
colors = video_processing.extract_colors(
|
|
224
|
+
self.video_path, self.start_frame, self.end_frame, self.mock_color_extractor
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# Assert that all frames were processed since target_frames was not given
|
|
228
|
+
self.assertEqual(len(colors), self.end_frame)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
if __name__ == "__main__":
|
|
232
|
+
unittest.main()
|