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
94
95
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)
    # stdout=PIPE guarantees a stream; the annotation is Optional for the general case
    assert process.stdout is not None, "ffmpeg was started without a readable output stream"

    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

crop_box_from_points

crop_box_from_points(points)

Turn the two dragged corners into an ffmpeg crop box.

The corners arrive in the order they were clicked, so either may be the top-left one; the box is normalised here rather than at each call site.

Parameters:

Name Type Description Default
points list

The two (x, y) corners of the drawn rectangle.

required

Returns:

Name Type Description
tuple tuple[int, int, int, int]

(w, h, x, y) — the box width, height, and top-left corner.

Raises:

Type Description
ValueError

If fewer than two corners were recorded, i.e. no rectangle was drawn.

Source code in musicalgestures/_cropvideo.py
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
def crop_box_from_points(points: list) -> tuple[int, int, int, int]:
    """Turn the two dragged corners into an ffmpeg crop box.

    The corners arrive in the order they were clicked, so either may be the
    top-left one; the box is normalised here rather than at each call site.

    Args:
        points (list): The two (x, y) corners of the drawn rectangle.

    Returns:
        tuple: (w, h, x, y) — the box width, height, and top-left corner.

    Raises:
        ValueError: If fewer than two corners were recorded, i.e. no rectangle
            was drawn.
    """
    if len(points) < 2:
        raise ValueError(
            "No crop area was selected. Drag a rectangle across the frame with the left mouse "
            "button held down, then press 'c' to crop.")

    (x_start, y_start), (x_stop, y_stop) = points[0], points[1]

    if x_stop < x_start:
        x_start, x_stop = x_stop, x_start
    if y_stop < y_start:
        y_start, y_stop = y_stop, y_start

    return x_stop - x_start, y_stop - y_start, x_start, y_start

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
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
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
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
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
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