diff --git a/README.md b/README.md index 22927de..0e2673e 100644 --- a/README.md +++ b/README.md @@ -53,13 +53,20 @@ usage: movie-barcodes [-h] -i INPUT_VIDEO_PATH [-d [DESTINATION_PATH]] [-t {hori - `-w`, `--workers`: Number of parallel workers for processing. By default, the script will use all available CPU cores. Setting this to 1 will use sequential processing. (Optional, type: int) -- `--width`: The output image's width in pixels. If not specified, the width will be the same as the input video. (Optional, type: int) +- `--width`: For horizontal barcodes, sets both (1) the number of sampled frames and (2) the output image width in pixels. If not specified, defaults to the input video width. For circular barcodes, this flag is ignored (see notes). (Optional, type: int) - `--height`: The output image's height in pixels. If not specified, the height will be the same as the input video. (Optional, type: int) - `-n`, `--output_name`: Custom name for the output barcode image. If not provided, a name will be automatically generated. (Optional, type: str) -- `-a`, `--all_methods`: If set to True, all methods for color extraction will be employed, overriding the --method argument. Default is False. (Optional, type: bool) +- `-a`, `--all_methods`: If set, all extraction methods will be run. This overrides `--method` and produces one image per method. Default is False. (Optional, type: bool) + +Notes: +- Circular barcode sizing: circular barcode diameter uses the input video width by default. `--width`/`--height` do not apply to circular barcodes. +- Destination paths: + - If a relative path is provided, it is resolved relative to the project root. + - If only a filename (basename) is provided, it is saved in the project root. + - Parent directories are created automatically when saving the image. # Examples ## Sequential Processing diff --git a/src/movie_barcodes/__init__.py b/src/movie_barcodes/__init__.py index 029315d..d2e68f9 100644 --- a/src/movie_barcodes/__init__.py +++ b/src/movie_barcodes/__init__.py @@ -3,7 +3,6 @@ This package provides CLI and library functions to generate movie color barcodes. """ -from . import barcode_generation as barcode from . import barcode_generation as barcode_generation from . import color_extraction from . import video_processing @@ -11,7 +10,6 @@ from . import utility __all__ = [ - "barcode", "barcode_generation", "color_extraction", "video_processing", diff --git a/src/movie_barcodes/barcode_generation.py b/src/movie_barcodes/barcode_generation.py index 33539e2..791db26 100644 --- a/src/movie_barcodes/barcode_generation.py +++ b/src/movie_barcodes/barcode_generation.py @@ -10,10 +10,10 @@ def generate_circular_barcode(colors: list, img_size: int, scale_factor: int = 1 """ Generate a circular barcode from the list of colors or smoothed frames. - :param list colors: List of RGB colors or smoothed frames. + :param list colors: List of BGR colors or smoothed frames. :param int img_size: The size of the square image (both width and height). :param int scale_factor: The scale factor to use when generating the barcode. Default is 10. - :return: np.ndarray: Circular barcode image. + :return: np.ndarray: Circular barcode image (BGRA; converted to RGBA when saving). """ high_res_img_size = img_size * scale_factor @@ -26,13 +26,7 @@ def generate_circular_barcode(colors: list, img_size: int, scale_factor: int = 1 max_radius = center # The largest circle's radius will be half of the image size radius_increment = max_radius / total_circles - for idx, color in tqdm( - enumerate(colors), - desc="Generating Barcode", - total=len(colors), - unit="%", - bar_format="{l_bar}{bar}| {percentage:3.0f}% [{elapsed}<{remaining}]", - ): + for idx, color in tqdm(enumerate(colors), desc="Generating Barcode", total=len(colors), unit="it"): radius = (idx + 1) * radius_increment # Handle both simple BGR tuples and smoothed frames @@ -61,11 +55,12 @@ def generate_barcode( ) -> np.ndarray: """ Generate a barcode image based on dominant colors or smoothed frames of video frames. - :param list colors: List of dominant colors or smoothed frames from video frames. + Colors are treated as BGR internally and converted to RGB once at save-time. + :param list colors: List of dominant BGR colors or smoothed frames from video frames. :param int frame_height: The height of the barcode image. :param int frame_count: The total number of frames in the video. :param Optional[int] frame_width: The width of the barcode image. If not specified, defaults to frame_count. - :return: np.ndarray: A barcode image. + :return: np.ndarray: A barcode image (BGR). """ if frame_width is None: frame_width = frame_count @@ -74,13 +69,7 @@ def generate_barcode( step = max(1, len(colors) // frame_width) sampled_colors = [colors[i] for i in range(0, len(colors), step)] - for i, color in tqdm( - enumerate(sampled_colors), - desc="Generating Barcode", - total=len(sampled_colors), - unit="%", - bar_format="{l_bar}{bar}| {percentage:3.0f}% [{elapsed}<{remaining}]", - ): + for i, color in tqdm(enumerate(sampled_colors), desc="Generating Barcode", total=len(sampled_colors), unit="it"): if i < frame_width: if isinstance(color, np.ndarray) and color.ndim == 3 and color.shape[1] == 1: # For smoothed frames diff --git a/src/movie_barcodes/utility.py b/src/movie_barcodes/utility.py index d2c91f3..a63ebeb 100644 --- a/src/movie_barcodes/utility.py +++ b/src/movie_barcodes/utility.py @@ -153,6 +153,9 @@ def save_barcode_image(barcode: np.ndarray, base_name: str, args: argparse.Names destination_path = args.destination_path if not path.isabs(destination_path): destination_path = path.join(project_root, destination_path) + # Ensure parent directory exists if user provided a custom path + parent_dir = path.dirname(destination_path) or "." + ensure_directory(parent_dir) if barcode.shape[2] == 4: # If the image has an alpha channel (RGBA) # Convert BGRA -> RGBA once at save-time diff --git a/src/movie_barcodes/video_processing.py b/src/movie_barcodes/video_processing.py index df03d0e..4f80a6b 100644 --- a/src/movie_barcodes/video_processing.py +++ b/src/movie_barcodes/video_processing.py @@ -128,9 +128,10 @@ def extract_colors( for _ in tqdm(range(target_frames or total_frames), desc="Processing frames"): ret, frame = video.read() # Read the first or next frame - if ret: - dominant_color = color_extractor(frame) - colors.append(dominant_color) + if not ret: + break + dominant_color = color_extractor(frame) + colors.append(dominant_color) for _ in range(frame_skip - 1): video.grab() # Skip frames diff --git a/tests/test_utility.py b/tests/test_utility.py index 5d7c21d..18927a6 100644 --- a/tests/test_utility.py +++ b/tests/test_utility.py @@ -366,11 +366,16 @@ def test_save_barcode_image_with_relative_destination_path( When a relative destination_path is provided, ensure it is resolved relative to project root. """ mock_abspath.return_value = "/fake/root/src/movie_barcodes/utility.py" - # save_barcode_image calls dirname three times: dirname(abspath(...)) and dirname(dirname(current_dir)) + # save_barcode_image calls dirname four times: + # 1) dirname(abspath(...)) => current_dir + # 2) dirname(current_dir) + # 3) dirname() => project root + # 4) dirname(resolved destination_path) => parent dir mock_dirname.side_effect = [ "/fake/root/src/movie_barcodes", # dirname of abspath "/fake/root/src", # inner dirname(current_dir) "/fake/root", # outer dirname() => project root + "/fake/root/relative", # parent dir of destination_path ] mock_isabs.return_value = False mock_path_join.side_effect = lambda *args: "/".join(args)