Skip to content

Videograms

videograms_ffmpeg

videograms_ffmpeg(self, target_name_x=None, target_name_y=None, overwrite=True, mode='average', line_x=None, line_y=None)

Renders horizontal and vertical videograms of the source video using ffmpeg. By default, videoframes are averaged by axes. Alternatively, mode='slit' samples a single column and row per frame (photo-finish style) and stacks those over time.

Parameters:

Name Type Description Default
target_name_x str

Target output name for the vertical videogram (the x-axis collapse). Defaults to None (which assumes that the input filename with the suffix "_vgv" should be used).

None
target_name_y str

Target output name for the horizontal videogram (the y-axis collapse). Defaults to None (which assumes that the input filename with the suffix "_vgh" should be used).

None
overwrite bool

Whether to allow overwriting existing files or to automatically increment target filenames to avoid overwriting. Defaults to True.

True
mode str

Either 'average' (default) or 'slit'. 'slit' uses one column/row line per frame instead of averaging.

'average'
line_x int

Source x-position (column index) used in slit mode for the vertical videogram. Defaults to the center column.

None
line_y int

Source y-position (row index) used in slit mode for the horizontal videogram. Defaults to the center row.

None

Returns:

Name Type Description
MgList 'MgList'

An MgList with the MgImage objects referring to the vertical and horizontal videograms respectively.

Source code in musicalgestures/_videograms.py
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
def videograms_ffmpeg(
    self,
    target_name_x: str | None = None,
    target_name_y: str | None = None,
    overwrite: bool = True,
    mode: str = "average",
    line_x: int | None = None,
    line_y: int | None = None,
) -> "MgList":
    """
    Renders horizontal and vertical videograms of the source video using ffmpeg.
    By default, videoframes are averaged by axes. Alternatively, ``mode='slit'``
    samples a single column and row per frame (photo-finish style) and stacks
    those over time.

    Args:
        target_name_x (str, optional): Target output name for the vertical videogram (the x-axis collapse). Defaults to None (which assumes that the input filename with the suffix "_vgv" should be used).
        target_name_y (str, optional): Target output name for the horizontal videogram (the y-axis collapse). Defaults to None (which assumes that the input filename with the suffix "_vgh" should be used).
        overwrite (bool, optional): Whether to allow overwriting existing files or to automatically increment target filenames to avoid overwriting. Defaults to True.
        mode (str, optional): Either ``'average'`` (default) or ``'slit'``.
            ``'slit'`` uses one column/row line per frame instead of averaging.
        line_x (int, optional): Source x-position (column index) used in slit
            mode for the vertical videogram. Defaults to the center column.
        line_y (int, optional): Source y-position (row index) used in slit mode
            for the horizontal videogram. Defaults to the center row.

    Returns:
        MgList: An MgList with the MgImage objects referring to the vertical and horizontal videograms respectively. 
    """

    width, height = get_widthheight(self.filename)
    framecount = get_framecount(self.filename)
    mode = mode.lower()

    if mode not in ["average", "slit"]:
        raise ValueError("mode must be 'average' or 'slit'.")

    def _resolve_line(line: int | None, max_size: int, name: str, default: int) -> int:
        if line is None:
            return default
        if isinstance(line, bool) or not isinstance(line, int):
            raise ValueError(f"{name} must be an integer in the range [0, {max_size - 1}].")
        if line < 0 or line >= max_size:
            raise ValueError(f"{name} must be an integer in the range [0, {max_size - 1}].")
        return line

    slit_x = width // 2
    slit_y = height // 2
    if mode == "slit":
        slit_x = _resolve_line(line_x, width, "line_x", width // 2)
        slit_y = _resolve_line(line_y, height, "line_y", height // 2)

    def calc_skipfactor(width, height, framecount):
        """
        Helper function to calculate the necessary frame-skipping to avoid integer overflow. This makes sure that we can succesfully create videograms even on many-hours-long videos as well.

        Args:
            width (int): The width of the video.
            height (int): The height of the video.
            framecount (int): The number of frames in the video.

        Returns:
            list(int, int): The necessary dilation factors to apply on the video for the horizontal and vertical videograms, respectively.
        """

        intmax = 2147483647
        skipfactor_x = int(
            math.ceil(framecount*8 / (intmax / (height+128) - 1024)))
        skipfactor_y = int(
            math.ceil(framecount / (intmax / ((width*8)+1024) - 128)))
        return skipfactor_x, skipfactor_y

    testx, testy = calc_skipfactor(width, height, framecount)

    if mode == "average":
        suffix_x, suffix_y = "_vgv.png", "_vgh.png"
    else:
        suffix_x, suffix_y = "_vgv_slit.png", "_vgh_slit.png"

    def _filters(framecount: int) -> tuple[str, str]:
        if mode == "average":
            return (
                f"scale=1:{height}:sws_flags=area,normalize,tile={framecount}x1",
                f"scale={width}:1:sws_flags=area,normalize,tile=1x{framecount}",
            )
        return (
            f"format=rgb24,crop=1:{height}:{slit_x}:0,normalize,tile={framecount}x1",
            f"format=rgb24,crop={width}:1:0:{slit_y},normalize,tile=1x{framecount}",
        )

    if testx > 1 or testy > 1:
        necessary_skipfactor = max([testx, testy])
        print(f'{os.path.basename(self.filename)} is too large to process. Applying minimal skipping necessary...')

        shortened_file = skip_frames_ffmpeg(self.filename, skip=necessary_skipfactor-1)
        skip_of = os.path.splitext(shortened_file)[0]
        framecount = get_framecount(shortened_file)
        length = get_length(shortened_file)
        vf_y, vf_x = _filters(framecount)

        target_name_x = resolve_filename(skip_of, suffix_x, target_name_x, overwrite)
        target_name_y = resolve_filename(skip_of, suffix_y, target_name_y, overwrite)

        cmd = ['ffmpeg', '-y', '-i', shortened_file, '-vf',
               vf_y, '-aspect', f'{framecount}:{height}', '-frames', '1', target_name_y]
        ffmpeg_cmd(cmd, length, stream=False, pb_prefix="Rendering horizontal videogram:")

        cmd = ['ffmpeg', '-y', '-i', shortened_file, '-vf',
               vf_x, '-aspect', f'{width}:{framecount}', '-frames', '1', target_name_x]
        ffmpeg_cmd(cmd, length, stream=False, pb_prefix="Rendering vertical videogram:")

        # save results as MgImages at self.video_gram_x and self.video_gram_y for parent MgObject
        self.videogram_x_image = MgImage(target_name_x)
        self.videogram_y_image = MgImage(target_name_y)

        # return MgList([MgImage(target_name_x), MgImage(target_name_y)])
        return MgList(self.videogram_x_image, self.videogram_y_image)


    else:
        length = get_length(self.filename)
        vf_y, vf_x = _filters(framecount)

        target_name_x = resolve_filename(self.of, suffix_x, target_name_x, overwrite)
        target_name_y = resolve_filename(self.of, suffix_y, target_name_y, overwrite)

        cmd = ['ffmpeg', '-y', '-i', self.filename, '-frames', '1', '-vf',
               vf_y, '-aspect', f'{framecount}:{height}', target_name_y]
        ffmpeg_cmd(cmd, length, stream=False, pb_prefix="Rendering horizontal videogram:")

        cmd = ['ffmpeg', '-y', '-i', self.filename, '-frames', '1', '-vf',
               vf_x, '-aspect', f'{width}:{framecount}', target_name_x]
        ffmpeg_cmd(cmd, length, stream=False, pb_prefix="Rendering vertical videogram:")

        # save results as MgImages at self.videogram_x_image and self.videogram_y_image for parent MgObject
        self.videogram_x_image = MgImage(target_name_x)
        self.videogram_y_image = MgImage(target_name_y)

        # return MgList([MgImage(target_name_x), MgImage(target_name_y)])
        return MgList(self.videogram_x_image, self.videogram_y_image)