Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 0 additions & 2 deletions src/movie_barcodes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@
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
from .cli import main as main
from . import utility

__all__ = [
"barcode",
"barcode_generation",
"color_extraction",
"video_processing",
Expand Down
25 changes: 7 additions & 18 deletions src/movie_barcodes/barcode_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
3 changes: 3 additions & 0 deletions src/movie_barcodes/utility.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions src/movie_barcodes/video_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
7 changes: 6 additions & 1 deletion tests/test_utility.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(<above>) => 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(<above>) => project root
"/fake/root/relative", # parent dir of destination_path
]
mock_isabs.return_value = False
mock_path_join.side_effect = lambda *args: "/".join(args)
Expand Down