Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add options to set background color when exporting video #1328

Merged
merged 12 commits into from
Sep 18, 2023
3 changes: 3 additions & 0 deletions docs/guides/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,9 @@ optional arguments:
--distinctly_color DISTINCTLY_COLOR
Specify how to color instances. Options include: "instances",
"edges", and "nodes" (default: "instances")
--background BACKGROUND
Specify the type of background to be used to save the videos.
Options: original, black, white and grey. (default: "original")
```

## Debugging
Expand Down
4 changes: 4 additions & 0 deletions sleap/config/labeled_clip_form.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ main:
label: Use GUI Visual Settings (colors, line widths)
type: bool
default: true
- name: background
label: Video Background
type: list
options: original,black,white,grey
- name: open_when_done
label: Open When Done Saving
type: bool
Expand Down
2 changes: 2 additions & 0 deletions sleap/gui/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -1295,6 +1295,7 @@ def do_action(context: CommandContext, params: dict):
frames=list(params["frames"]),
fps=params["fps"],
color_manager=params["color_manager"],
background=params["background"],
show_edges=params["show edges"],
edge_is_wedge=params["edge_is_wedge"],
marker_size=params["marker size"],
Expand Down Expand Up @@ -1354,6 +1355,7 @@ def ask(context: CommandContext, params: dict) -> bool:
params["fps"] = export_options["fps"]
params["scale"] = export_options["scale"]
params["open_when_done"] = export_options["open_when_done"]
params["background"] = export_options["background"]

params["crop"] = None

Expand Down
3 changes: 2 additions & 1 deletion sleap/io/video.py
Original file line number Diff line number Diff line change
Expand Up @@ -1118,8 +1118,9 @@ def get_frames(self, idxs: Union[int, Iterable[int]]) -> np.ndarray:

def get_frames_safely(self, idxs: Iterable[int]) -> Tuple[List[int], np.ndarray]:
"""Return list of frame indices and frames which were successfully loaded.
Args:
idxs: An iterable object that contains the indices of frames.

idxs: An iterable object that contains the indices of frames.

Returns: A tuple of (frame indices, frames), where
* frame indices is a subset of the specified idxs, and
Expand Down
34 changes: 32 additions & 2 deletions sleap/io/visuals.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,13 @@
_sentinel = object()


def reader(out_q: Queue, video: Video, frames: List[int], scale: float = 1.0):
def reader(
out_q: Queue,
video: Video,
frames: List[int],
scale: float = 1.0,
background: str = "original",
):
"""Read frame images from video and send them into queue.

Args:
Expand All @@ -36,11 +42,13 @@ def reader(out_q: Queue, video: Video, frames: List[int], scale: float = 1.0):
video: The `Video` object to read.
frames: Full list frame indexes we want to read.
scale: Output scale for frame images.
background: output video background. Either original, black, white, grey

Returns:
None.
"""

background = background.lower()
cv2.setNumThreads(usable_cpu_count())

total_count = len(frames)
Expand All @@ -64,6 +72,16 @@ def reader(out_q: Queue, video: Video, frames: List[int], scale: float = 1.0):
loaded_chunk_idxs, video_frame_images = video.get_frames_safely(
frames_idx_chunk
)
if background != "original":
# fill the frame with the color
fill_values = {"black": 0, "grey": 127, "white": 255}
try:
fill = fill_values[background]
except KeyError:
raise ValueError(
f"Invalid background color: {background}. Options include: {', '.join(fill_values.keys())}"
)
video_frame_images = video_frame_images * 0 + fill
Comment on lines 72 to +84
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This block of code introduces a new feature to set the background color of the video. It checks if the provided background is not "original", and if so, it fills the frame with the specified color. However, there's no validation for the case when background is "original". If an invalid value is passed, the error will only be raised after some computation has already been done (i.e., after frames are loaded). To improve efficiency, consider validating the background argument at the beginning of the function.

+    fill_values = {"black": 0, "grey": 127, "white": 255, "original": None}
+    try:
+        fill = fill_values[background]
+    except KeyError:
+        raise ValueError(
+            f"Invalid background color: {background}. Options include: {', '.join(fill_values.keys())}"
+        )
     loaded_chunk_idxs, video_frame_images = video.get_frames_safely(
         frames_idx_chunk
     )
-    if background != "original":
-        # fill the frame with the color
-        fill_values = {"black": 0, "grey": 127, "white": 255}
-        try:
-            fill = fill_values[background]
-        except KeyError:
-            raise ValueError(
-                f"Invalid background color: {background}. Options include: {', '.join(fill_values.keys())}"
-            )
-        video_frame_images = video_frame_images * 0 + fill


if not loaded_chunk_idxs:
print(f"No frames could be loaded from chunk {chunk_i}")
Expand Down Expand Up @@ -497,6 +515,7 @@ def save_labeled_video(
fps: int = 15,
scale: float = 1.0,
crop_size_xy: Optional[Tuple[int, int]] = None,
background: str = "original",
show_edges: bool = True,
edge_is_wedge: bool = False,
marker_size: int = 4,
Expand All @@ -515,6 +534,7 @@ def save_labeled_video(
fps: Frames per second for output video.
scale: scale of image (so we can scale point locations to match)
crop_size_xy: size of crop around instances, or None for full images
background: output video background. Either original, black, white, grey
show_edges: whether to draw lines between nodes
edge_is_wedge: whether to draw edges as wedges (draw as line if False)
marker_size: Size of marker in pixels before scaling by `scale`
Expand All @@ -537,7 +557,7 @@ def save_labeled_video(
q2 = Queue(maxsize=10)
progress_queue = Queue()

thread_read = Thread(target=reader, args=(q1, video, frames, scale))
thread_read = Thread(target=reader, args=(q1, video, frames, scale, background))
thread_mark = VideoMarkerThread(
in_q=q1,
out_q=q2,
Expand Down Expand Up @@ -695,6 +715,15 @@ def main(args: list = None):
"and 'nodes' (default: 'nodes')"
),
)
parser.add_argument(
"--background",
type=str,
default="original",
help=(
"Specify the type of background to be used to save the videos."
"Options for background: original, black, white and grey"
),
)
args = parser.parse_args(args=args)
labels = Labels.load_file(
args.data_path, video_search=[os.path.dirname(args.data_path)]
Expand Down Expand Up @@ -730,6 +759,7 @@ def main(args: list = None):
marker_size=args.marker_size,
palette=args.palette,
distinctly_color=args.distinctly_color,
background=args.background,
)

print(f"Video saved as: {filename}")
Expand Down
41 changes: 41 additions & 0 deletions tests/io/test_visuals.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import numpy as np
import os
import pytest
import cv2
from sleap.io.dataset import Labels
from sleap.io.visuals import (
save_labeled_video,
Expand Down Expand Up @@ -63,6 +64,46 @@ def test_serial_pipeline(centered_pair_predictions, tmpdir):
)


@pytest.mark.parametrize("background", ["original", "black", "white", "grey"])
def test_sleap_render_with_different_backgrounds(background):
args = (
f"-o test_{background}.avi -f 2 --scale 1.2 --frames 1,2 --video-index 0 "
f"--background {background} "
"tests/data/json_format_v2/centered_pair_predictions.json".split()
)
sleap_render(args)
assert (
os.path.exists(f"test_{background}.avi")
and os.path.getsize(f"test_{background}.avi") > 0
)

# Check if the background is set correctly if not original background
if background != "original":
saved_video_path = f"test_{background}.avi"
cap = cv2.VideoCapture(saved_video_path)
ret, frame = cap.read()

# Calculate mean color of the channels
b, g, r = cv2.split(frame)
mean_b = np.mean(b)
mean_g = np.mean(g)
mean_r = np.mean(r)

# Set threshold values. Color is white if greater than white threshold, black
# if less than grey threshold and grey if in between both threshold values.
white_threshold = 240
grey_threshold = 40

# Check if the average color is white, grey, or black
if all(val > white_threshold for val in [mean_b, mean_g, mean_r]):
background_color = "white"
elif all(val < grey_threshold for val in [mean_b, mean_g, mean_r]):
background_color = "black"
else:
background_color = "grey"
assert background_color == background
Comment on lines +67 to +104
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new test function test_sleap_render_with_different_backgrounds is a good addition to the test suite. It checks whether the video rendering with different background colors works as expected. However, there's a potential issue with the way the average color of the frame is calculated and compared to the threshold values. The mean color calculation does not take into account the distribution of the colors in the frame. A frame with a majority of white pixels and some black pixels could still have an average color that is grey. Consider using a more robust method for determining the predominant color in the frame.

-         # Calculate mean color of the channels
-         b, g, r = cv2.split(frame)
-         mean_b = np.mean(b)
-         mean_g = np.mean(g)
-         mean_r = np.mean(r)
+         # Calculate the most frequent color of the channels
+         b, g, r = cv2.split(frame)
+         mode_b = scipy.stats.mode(b, axis=None)[0][0]
+         mode_g = scipy.stats.mode(g, axis=None)[0][0]
+         mode_r = scipy.stats.mode(r, axis=None)[0][0]



def test_sleap_render(centered_pair_predictions):
args = (
"-o testvis.avi -f 2 --scale 1.2 --frames 1,2 --video-index 0 "
Expand Down
Loading