Skip to content

Commit

Permalink
added vector motion moshing with python
Browse files Browse the repository at this point in the history
  • Loading branch information
tiberiuiancu committed Mar 25, 2021
1 parent 6af48ea commit 22a2217
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 21 deletions.
37 changes: 30 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,57 @@ Datamoshing made easy.

## Requirements
`mosh.py` requires `ffmpeg` to be installed.
`vector_motion.py` and `style_transfer.py` rely on `ffedit` and `ffgac`, which can be downloaded from [ffglitch.org](https://ffglitch.org/)
`vector_motion.py` and `style_transfer.py` rely on `ffedit` and `ffgac`, which can be downloaded from
[ffglitch.org](https://ffglitch.org/)

# Effects

## I frame removal
This type of glitch creates the transition effect. Example:

$ python mosh.py input.mp4 -s 40 -e 90 -o output.mp4
removes all the i-frames from the input video starting at frame 40 and ending at frame 90, and outputs the final result to `output.mp4`
removes all the i-frames from the input video starting at frame 40 and ending at frame 90, and outputs the final result
to `output.mp4`

## Delta frame duplication
Repeats a series of p-frames (aka delta frames), which can give a 'melting' effect. This type of glitch is triggered by the `-d` flag. Example:
Repeats a series of p-frames (aka delta frames), which can give a 'melting' effect. This type of glitch is triggered by
the `-d` flag. Example:

$ python mosh.py input.mp4 -d 30 -s 30 -e 120 -o output.mp4

copies frames with indexes [30, 59] and overwrites frames [60, 89] and [90, 119] with the copied data.

## Vector motion
While the previous effects copy and delete whole frames, this one changes the actual frame data. As explained in [this article on ffglitch.org](https://ffglitch.org/2020/07/mv.html), you need to write a custom JavaScript file that can change the frame data. `vector_motion.py` is just a wrapper for `ffedit` and `ffgac` and makes moshing possible through only one command.
While the previous effects copy and delete whole frames, this one changes the actual frame data. As explained in
[this article on ffglitch.org](https://ffglitch.org/2020/07/mv.html), you need to write a custom JavaScript file
that can change the frame data. `vector_motion.py` is just a wrapper for `ffedit` and `ffgac` and makes moshing
possible through only one command.
Example:

$ python vector_motion.py input.mp4 -s your_script.js -o output.mp4

I am planning to add support for moshing using python scripts in the near future (but this will be done internally through a js script).

### Now also with Python!

If you prefer to use python to glitch the frames instead, you can also specify a python script for the `-s` option.
The script must contain a function called `mosh_frames` that takes as argument an array of frames (warning: some of the frames
might be empty), where each non-empty frame represents a 3D array of shape (height, width, 2). The function should
return an array containing the modified vectors.

`horizontal_motion_example.py` contains the equivalent python script of the js script presented in the
[ffglitch tutorial](https://ffglitch.org/2020/07/mv.html).

`average_motion_example.py` is the equivalent of this [ffglitch average motion tutorial](https://ffglitch.org/2020/07/mv_avg.html)
using numpy. Neat!


## Style transfer
I call style transfer combining the motion vectors of two videos (by simply adding them together). For example, applying the vector motion data of a person talking to a video of clouds can make it look as though the clouds are talking.
This script can also extract motion vector data from a video and write it to a file, or read motion data from file and apply it to a video.
I call style transfer combining the motion vectors of two videos (by simply adding them together). For example,
applying the vector motion data of a person talking to a video of clouds can make it look as though the clouds
are talking.

This script can also extract motion vector data from a video and write it to a file, or read motion data from file and
apply it to a video.

Examples:

Expand Down
14 changes: 14 additions & 0 deletions average_motion_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import numpy as np


average_length = 10


def average(frames):
if not frames:
return []
return np.mean(np.array([x for x in frames if x != []]), axis=0).tolist()


def mosh_frames(frames):
return [average(frames[i + 1 - average_length: i + 1]) for i in range(len(frames))]
11 changes: 11 additions & 0 deletions horizontal_motion_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
def mosh_frames(frames):
for frame in frames:
if not frame:
continue

for row in frame:
for col in row:
# col contains the horizontal and vertical components of the vector
col[0] = 0

return frames
55 changes: 41 additions & 14 deletions vector_motion.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,49 @@
import argparse
import subprocess
import os
import importlib.util as imp
from vector_util import get_vectors, apply_vectors

parser = argparse.ArgumentParser()
parser.add_argument('input_video', type=str, help='specifies input file')
parser.add_argument('-s', type=str, dest='script_path', help='path to the js script', required=True)
parser.add_argument('-g', type=str, default=1000, dest='gop_period', help='I-frame period (in frames)')
parser.add_argument('-o', default='moshed.mpg', type=str, dest='output_video', help='output file for the moshed video')
args = parser.parse_args().__dict__

input_video = args['input_video']
gop_period = args['gop_period']
output_video = args['output_video']
script_path = args['script_path']
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument('input_video', type=str, help='specifies input file')
parser.add_argument('-s', type=str, dest='script_path', help='path to the script', required=True)
parser.add_argument('-g', type=str, default=1000, dest='gop_period', help='I-frame period (in frames)')
parser.add_argument('-o', default='moshed.mpg', type=str, dest='output_video',
help='output file for the moshed video')
return parser.parse_args().__dict__

subprocess.call(f'ffgac -i {input_video} -an -mpv_flags +nopimb+forcemv -qscale:v 0 -g {gop_period}' +
' -vcodec mpeg2video -f rawvideo -y tmp.mpg', shell=True)

subprocess.call(f'ffedit -i tmp.mpg -f mv -s {script_path} -o {output_video}', shell=True)
def get_moshing_function(path):
spec = imp.spec_from_file_location('mod', path)
mod = imp.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod.mosh_frames

os.remove('tmp.mpg')

if __name__ == '__main__':
args = parse_args()
input_video = args['input_video']
gop_period = args['gop_period']
output_video = args['output_video']
script_path = args['script_path']
script_type = script_path[-2:]

if script_type == 'py':
try:
# import the function responsible for moshing from the specified script
func = get_moshing_function(script_path)

# method = '' specifies to just copy the new vectors over to the original video
apply_vectors(func(get_vectors(input_video)), input_video, output_video, method='')
except Exception as e:
# TODO: proper error handling
print(f'couldn\'t apply function mosh_frames from script: {script_path}:\n{e}')
exit(0)
else:
subprocess.call(f'ffgac -i {input_video} -an -mpv_flags +nopimb+forcemv -qscale:v 0 -g {gop_period}' +
' -vcodec mpeg2video -f rawvideo -y tmp.mpg', shell=True)

subprocess.call(f'ffedit -i tmp.mpg -f mv -s {script_path} -o {output_video}', shell=True)
os.remove('tmp.mpg')

0 comments on commit 22a2217

Please # to comment.