Skip to content

Cropvideo

find_motion_box_ffmpeg

find_motion_box_ffmpeg(filename, motion_box_thresh=0.1, motion_box_margin=12)

Helper function to find the area of motion in a video, using ffmpeg.

Parameters:

Name Type Description Default
filename str

Path to the video file.

required
motion_box_thresh float

Pixel threshold to apply to the video before assessing the area of motion. Defaults to 0.1.

0.1
motion_box_margin int

Margin (in pixels) to add to the detected motion box. Defaults to 12.

12

Raises:

Type Description
KeyboardInterrupt

In case we stop the process manually.

Returns:

Name Type Description
int

The width of the motion box.

int

The height of the motion box.

int

The X coordinate of the top left corner of the motion box.

int

The Y coordinate of the top left corner of the motion box.

Source code in musicalgestures/_cropvideo.py
 8
 9
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
def find_motion_box_ffmpeg(filename: str, motion_box_thresh: float = 0.1, motion_box_margin: int = 12):
    """
    Helper function to find the area of motion in a video, using ffmpeg.

    Args:
        filename (str): Path to the video file.
        motion_box_thresh (float, optional): Pixel threshold to apply to the video before assessing the area of motion. Defaults to 0.1.
        motion_box_margin (int, optional): Margin (in pixels) to add to the detected motion box. Defaults to 12.

    Raises:
        KeyboardInterrupt: In case we stop the process manually.

    Returns:
        int: The width of the motion box.
        int: The height of the motion box.
        int: The X coordinate of the top left corner of the motion box.
        int: The Y coordinate of the top left corner of the motion box.
    """

    import subprocess
    import matplotlib
    import numpy as np
    total_time = get_length(filename)
    width, height = get_widthheight(filename)
    crop_str = ''

    thresh_color = matplotlib.colors.to_hex(
        [motion_box_thresh, motion_box_thresh, motion_box_thresh])
    thresh_color = '0x' + thresh_color[1:]

    pb = MgProgressbar(total=total_time, prefix='Finding area of motion:')

    command = ['ffmpeg', '-y', '-i', filename, '-f', 'lavfi', '-i', f'color={thresh_color},scale={width}:{height}', '-f', 'lavfi', '-i', f'color=black,scale={width}:{height}', '-f',
               'lavfi', '-i', f'color=white,scale={width}:{height}', '-lavfi', 'format=gray,tblend=all_mode=difference,threshold,cropdetect=round=2:limit=0:reset=0', '-f', 'null', '/dev/null']

    process = subprocess.Popen(
        command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True)

    try:
        while True:
            out = process.stdout.readline()
            if out == '':
                process.wait()
                break
            else:
                out_list = out.split()
                crop_and_time = sorted(
                    [elem for elem in out_list if elem.startswith('t:') or elem.startswith('crop=')])
                if len(crop_and_time) != 0:
                    crop_str = crop_and_time[0]
                    time_float = float(crop_and_time[1][2:])
                    pb.progress(time_float)

        pb.progress(total_time)

        crop_width, crop_height, crop_x, crop_y = [
            int(elem) for elem in crop_str[5:].split(':')]

        motion_box_margin = roundup(motion_box_margin, 4)

        crop_width = np.clip(crop_width+motion_box_margin, 4, width)
        crop_height = np.clip(crop_height+motion_box_margin, 4, height)
        crop_x = np.clip(crop_x-(motion_box_margin/2), 4, width)
        crop_y = np.clip(crop_y-(motion_box_margin/2), 4, height)

        if crop_x + crop_width > width:
            crop_x = width - crop_width
        else:
            crop_x = np.clip(crop_x, 0, width)
        if crop_y + crop_height > height:
            crop_y = height - crop_height
        else:
            crop_y = np.clip(crop_y, 0, height)

        crop_width, crop_height, crop_x, crop_y = [
            int(elem) for elem in [crop_width, crop_height, crop_x, crop_y]]

        return crop_width, crop_height, crop_x, crop_y

    except KeyboardInterrupt:
        try:
            process.terminate()
        except OSError:
            pass
        process.wait()
        raise KeyboardInterrupt

mg_cropvideo_ffmpeg

mg_cropvideo_ffmpeg(filename, crop_movement='Auto', motion_box_thresh=0.1, motion_box_margin=12, target_name=None, overwrite=True)

Crops the video using ffmpeg.

Parameters:

Name Type Description Default
filename str

Path to the video file.

required
crop_movement str

'Auto' finds the bounding box that contains the total motion in the video. Motion threshold is given by motion_box_thresh. 'Manual' opens up a simple GUI that is used to crop the video manually by looking at the first frame. Defaults to 'Auto'.

'Auto'
motion_box_thresh float

Only meaningful if crop_movement='Auto'. Takes floats between 0 and 1, where 0 includes all the motion and 1 includes none. Defaults to 0.1.

0.1
motion_box_margin int

Only meaningful if crop_movement='Auto'. Adds margin to the bounding box. Defaults to 12.

12
target_name str

The name of the output video. Defaults to None (which assumes that the input filename with the suffix "_crop" 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

Returns:

Name Type Description
str

Path to the cropped video.

Source code in musicalgestures/_cropvideo.py
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
def mg_cropvideo_ffmpeg(
        filename: str,
        crop_movement: str = 'Auto',
        motion_box_thresh: float = 0.1,
        motion_box_margin: int = 12,
        target_name: str | None = None,
        overwrite: bool = True):
    """
    Crops the video using ffmpeg.

    Args:
        filename (str): Path to the video file.
        crop_movement (str, optional): 'Auto' finds the bounding box that contains the total motion in the video. Motion threshold is given by motion_box_thresh. 'Manual' opens up a simple GUI that is used to crop the video manually by looking at the first frame. Defaults to 'Auto'.
        motion_box_thresh (float, optional): Only meaningful if `crop_movement='Auto'`. Takes floats between 0 and 1, where 0 includes all the motion and 1 includes none. Defaults to 0.1.
        motion_box_margin (int, optional): Only meaningful if `crop_movement='Auto'`. Adds margin to the bounding box. Defaults to 12.
        target_name (str, optional): The name of the output video. Defaults to None (which assumes that the input filename with the suffix "_crop" should be used).
        overwrite (bool, optional): Whether to allow overwriting existing files or to automatically increment target filenames to avoid overwriting. Defaults to True.

    Returns:
        str: Path to the cropped video.
    """

    global x, y, w, h

    pb = MgProgressbar(total=get_length(filename), prefix='Rendering cropped video:')

    if crop_movement.lower() == 'manual':
        if not in_colab():
            import sys
            import subprocess
            import musicalgestures

            scale_ratio = get_box_video_ratio(filename)
            width, height = get_widthheight(filename)
            scaled_width, scaled_height = [int(elem * scale_ratio) for elem in [width, height]]
            first_frame_as_image = get_first_frame_as_image(filename, pict_format='.jpg')

            module_path = os.path.abspath(os.path.dirname(musicalgestures.__file__))
            pyfile = os.path.join(module_path, '_cropping_window.py')

            result = subprocess.run(
                [sys.executable, pyfile, first_frame_as_image, str(scale_ratio), str(scaled_width), str(scaled_height)],
                stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
            )

            os.remove(first_frame_as_image)

            if result.returncode != 0:
                raise RuntimeError(
                    f"Cropping window subprocess failed (exit code {result.returncode}):\n{result.stderr}"
                )

            res = result.stdout.strip()
            res_array = res.split(' ')
            if len(res_array) != 4:
                raise RuntimeError(
                    f"Unexpected output from cropping window: '{res}'"
                )
            w, h, x, y = [int(elem) for elem in res_array]

        else:
            x, y, w, h = manual_text_input()

    elif crop_movement.lower() == 'auto':
        w, h, x, y = find_motion_box_ffmpeg(filename, motion_box_thresh=motion_box_thresh, motion_box_margin=motion_box_margin)

    cropped_video = crop_ffmpeg(filename, w, h, x, y, target_name=target_name, overwrite=overwrite)

    return cropped_video

manual_text_input

manual_text_input()

Helper function for mg_crop_video_ffmpeg when its crop_movement is 'manual', but the environment is in Colab. In this case we can't display the windowed cropping UI, so we ask for the values as a text input.

Returns:

Name Type Description
list

x, y, w, h for crop_ffmpeg.

Source code in musicalgestures/_cropvideo.py
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
def manual_text_input():
    """
    Helper function for mg_crop_video_ffmpeg when its crop_movement is 'manual', but the environment is in Colab.
    In this case we can't display the windowed cropping UI, so we ask for the values as a text input.

    Returns:
        list: x, y, w, h for crop_ffmpeg. 
    """
    print("Looks like we are in Colab, can't run the cropping GUI here.")
    print("Please add the parameters of the cropping rectangle (in pixels): x, y, width, height")
    print("""
        x (int): The horizontal coordinate of the top left pixel of the cropping rectangle.
        y (int): The vertical coordinate of the top left pixel of the cropping rectangle.
        width (int): The desired width.
        height (int): The desired height.
    """)
    res = input()
    res = res.replace(",", " ")
    res_list = ' '.join(res.split()).split(" ")
    try:
        res_list_int = [abs(int(float(item))) for item in res_list]
    except ValueError:
        raise ValueError("Invalid parameter(s) found. Try only integer numbers.")

    if len(res_list_int) < 4:
        raise RuntimeError(f"Not enough parameters in {res_list_int}")

    res_list_int = res_list_int[:4]

    return res_list_int