Home

6. 🧮 Leveraging video analytics (e.g., plots, etc)

In this section we show you how to create plots that show the exact moments (i.e., timestamps) in the video when a specific event happens.

We select the video we wish to analyze

dataset = tenyks.get_dataset("popeye")

6.1 Plotting code (the MZ graph)

def convert_seconds(seconds):  
    minutes = seconds // 60  
    remaining_seconds = seconds % 60  
    return f"{minutes} minute(s) and {remaining_seconds} second(s)"
import numpy as np
from datetime import timedelta
import pandas as pd
import matplotlib.pyplot as plt

def adjust_timestamp(row, part_durations):
    part_index = row['video_key']
    return row['timestamp'] + sum(part_durations[part] for part in part_durations if part < part_index)

def calculate_total_duration(clips):
    return sum(clip.duration for clip in clips)

def merge_clips(clips):
    sorted_clips = sorted(clips, key=lambda clip: (clip.video_key, clip.timestamp))

    merged_clips = []
    current_clip = None

    for clip in sorted_clips:
        if current_clip is None:
            current_clip = clip
        else:
            current_end_time = current_clip.timestamp + current_clip.duration
            new_clip_start_time = clip.timestamp

            if current_clip.video_key == clip.video_key and current_end_time >= new_clip_start_time:
                current_clip.duration = max(current_clip.duration, new_clip_start_time + clip.duration - current_clip.timestamp)
            else:
                merged_clips.append(current_clip)
                current_clip = clip

    if current_clip:
        merged_clips.append(current_clip)

    return merged_clips

def merge_intervals_and_calculate_duration(df):
    merged_intervals = []
    current_start, current_end = df.loc[0, 'adjusted_start'], df.loc[0, 'adjusted_end']

    for i in range(1, len(df)):
        start = df.loc[i, 'adjusted_start']
        end = df.loc[i, 'adjusted_end']

        if start <= current_end:
            current_end = max(current_end, end)
        else:
            merged_intervals.append((current_start, current_end))
            current_start, current_end = start, end

    merged_intervals.append((current_start, current_end))

    total_duration = sum(end - start for start, end in merged_intervals)
    return total_duration, merged_intervals

def round_up_to_next_tick(value, tick_size):
    """Round up a value to the next multiple of tick_size."""
    return np.ceil(value / tick_size) * tick_size

import numpy as np
from datetime import timedelta
import pandas as pd
import matplotlib.pyplot as plt

# Step 1: Function to generate the plot and return bars
def plot_histogram_and_return_bars(movie_title, part_durations, clips, search_term_label="Search Term", bin_size_minutes=10):
    # First, merge the overlapping clips
    merged_clips = merge_clips(clips)

    # Create a DataFrame with the merged clips
    df_clips = pd.DataFrame(
        [{'video_key': clip.video_key, 'timestamp': clip.timestamp, 'duration': clip.duration} for clip in merged_clips])

    # Adjust timestamps based on part durations
    df_clips['adjusted_start'] = df_clips.apply(adjust_timestamp, axis=1, part_durations=part_durations)
    df_clips['adjusted_end'] = df_clips['adjusted_start'] + df_clips['duration']

    # Merge intervals and calculate the total duration
    total_duration, merged_intervals = merge_intervals_and_calculate_duration(df_clips)
    print(f"Total duration of {search_term_label} in \"{movie_title}\": {total_duration} seconds")

    # Calculate movie duration and bin size in seconds
    movie_duration = sum(part_durations.values())
    bin_size_seconds = bin_size_minutes * 60

    # Bin the merged intervals into bins of the specified size
    bins = np.arange(0, movie_duration + bin_size_seconds, bin_size_seconds)
    binned_durations = np.zeros(len(bins) - 1)
    first_timestamps = [None] * (len(bins) - 1)

    for start, end in merged_intervals:
        for i in range(len(bins) - 1):
            bin_start, bin_end = bins[i], bins[i + 1]
            if start < bin_end and end > bin_start:
                overlap_start = max(start, bin_start)
                overlap_end = min(end, bin_end)
                binned_durations[i] += overlap_end - overlap_start
                # Track the first timestamp
                if first_timestamps[i] is None or start < first_timestamps[i]:
                    first_timestamps[i] = start

    # Calculate cumulative duration for trend line
    cumulative_durations = np.cumsum(binned_durations)

    # Convert bin edges to time labels (hours:minutes:seconds)
    bin_labels = [str(timedelta(seconds=int(b))) for b in bins[:-1]]

    fig, ax1 = plt.subplots(figsize=(10, 5))

    # Plot the histogram (bar chart)
    bars = ax1.bar(bin_labels, binned_durations, color='skyblue', label=f'Duration of {search_term_label}', alpha=0.7)
    ax1.set_xlabel(f'Time Bins ({bin_size_minutes}-minute intervals)')
    ax1.set_ylabel('Total Duration in Bin (seconds)')
    ax1.tick_params(axis='x', rotation=45)

    # Add first timestamps on top of each bar
    for i, bar in enumerate(bars):
        if binned_durations[i] > 0:  # Only annotate if there is a bar
            first_ts = str(timedelta(seconds=int(first_timestamps[i])))
            ax1.text(bar.get_x() + bar.get_width() / 2, bar.get_height(), first_ts, ha='center', va='bottom', fontsize=9)

    # Create a second y-axis for the cumulative trend line
    ax2 = ax1.twinx()
    ax2.plot(bin_labels, cumulative_durations, 'k--', label='Cumulative Duration (Trend Line)')
    ax2.set_ylabel('Cumulative Duration (seconds)')

    # Round up the y-limits to the nearest tick (e.g., multiple of 50 or 100)
    tick_size = 100  # Define the tick size (e.g., 50 or 100)
    max_cumulative = round_up_to_next_tick(cumulative_durations[-1], tick_size)
    max_binned = round_up_to_next_tick(sum(binned_durations), tick_size)

    print(f"max_binned: {max_binned}, max_cumulative: {max_cumulative}")

    # Set the y-limits of both axes
    ax1.set_ylim(0, max_binned)
    ax2.set_ylim(0, max_cumulative)

    # Combine the legends from both axes
    ax1.legend(loc='upper left')
    ax2.legend(loc='upper right')

    # Set title and grid
    plt.title(f'Binned and Cumulative Duration of {search_term_label} Over Time in \"{movie_title}\"')
    ax1.grid(True)

    plt.tight_layout()  # Adjust layout so labels fit

    return fig, bars, ax1

# Step 2: Function to add arrows and text to specific bars
def add_arrows_and_text_to_bars(ax, bars, annotations, extra_space_factor=1.5, min_text_offset=0.5, arrow_offset=0.5):
    """
    Add arrows and text pointing to specific bars without overlapping with the text above the bars.
    :param ax: The axis of the plot.
    :param bars: List of bars in the plot.
    :param annotations: List of dictionaries containing information on where to annotate.
    :param extra_space_factor: Factor by which to increase the space between the bar and annotation for large bars.
    :param min_text_offset: Minimum vertical offset for text, even for small bars (to avoid too small arrows).
    :param arrow_offset: Offset to move the arrow's starting point higher above the bar's height to avoid overlap with text.
    """
    for ann in annotations:
        bar = bars[ann["bar_index"]]
        bar_height = bar.get_height()  # Get the height of the bar
        bar_x = bar.get_x() + bar.get_width() / 2  # Center of the bar

        # Calculate the text position using both the extra space factor and the minimum text offset
        text_y = bar_height + max(bar_height * extra_space_factor, min_text_offset)

        # Add the annotation with the arrow starting well above the bar and text placed accordingly
        ax.annotate(
            ann["text"],
            xy=(bar_x, bar_height + arrow_offset),  # Arrow starts well above the top of the bar
            xytext=(bar_x, text_y + arrow_offset),  # Text is placed above, keeping space with the arrow
            arrowprops=dict(
                facecolor='black',
                shrink=0.05 if bar_height > min_text_offset else 0.2,  # Adjust arrow length for small bars
                width=1,      # Thinner arrow line
                headwidth=5   # Arrowhead size
            ),
            ha='center', fontsize=10
        )

Giant Bird

Example: let's search for "giant bird" and plot the specific moments where this particular bird is shown in the previously retrieved video (i.e., "popeye").

We'll use the function search_video that can be found on our documentation.

search_video(n_videos = 50 , filter = None , sort_by =None , model_key = None)
NameTypeDescriptionDefault
n_videosOptional [str]Number of video clips to return. Defaults to 50.50
filterOptional [str]Filter conditions for the search. Defaults to None.None
sort_byOptional [str]Sort criteria for the search. Defaults to None.None
model_keyOptional [str]Model key to filter videos. Defaults to None.None

We use text search to retrieve 15 videos containing a "giant bird"

clips1 = dataset.search_video(sort_by="vector_text(giant bird)", n_videos=15)

We specify some parameters

movie_title = "Popeye the Sailer meets Sinbad the Sailor"  
part_durations = {  
    'Popeye_meetsSinbadtheSailor_512kb_mp4': 966.0 # total length of the video in seconds  
}  
bin_size_minutes=1

We can sort the results of search_video

data = []
clips_sorted = sorted(clips1, key=lambda clip: (clip.video_key, clip.timestamp))
for clip in clips_sorted:
    data.append({
        'video_key': clip.video_key,
        'timestamp_seconds': clip.timestamp,
        'timestamp_converted': convert_seconds(clip.timestamp),
        'duration_seconds': clip.duration,
        'duration_converted': convert_seconds(clip.duration)
    })

# Create a Pandas DataFrame from the list
df = pd.DataFrame(data)
df.head()

Finally, we plot the specific timestamps where a "giant bird" appears in the video

search_term_label = "Giant bird in Popeye"
fig, bars, ax1 = plot_histogram_and_return_bars(movie_title, part_durations, clips1, search_term_label, bin_size_minutes);
fig.set_size_inches(18, 6);
plt.close(fig)

""" Output
Total duration of Giant bird in Popeye in "Popeye the Sailer meets Sinbad the Sailor": 110.0 seconds
max_binned: 200.0, max_cumulative: 200.0
"""
annotations = [
    {"text": "Sindbad introduces the giant bird", "bar_index": 3},
    {"text": "Giant bird takes flight", "bar_index": 5},
    {"text": "Giant bird attacks Popeye's ship", "bar_index": 6},
    {"text": "Popeye defeats the giant bird", "bar_index": 11}
]

# Add the arrows and text to the existing plot
add_arrows_and_text_to_bars(ax1, bars, annotations, extra_space_factor=2.5, min_text_offset=10, arrow_offset=10)

# Re-display the plot with annotations
fig